[ { "contract_name": "AToken", "file_name": "AToken.txt", "file_path": "https://github.com/aave/protocol-v2/blob/750920303e33b66bc29862ea3b85206dda9ce786/contracts/protocol/tokenization/AToken.sol", "metadata": { "license": "agpl-3.0", "solidity_version": "0.6.12", "description": "Implementation of the interest bearing token for the Aave protocol", "author": "Aave" }, "state_variables": [ { "name": "EIP712_REVISION", "type": "bytes", "visibility": "public", "mutability": "constant", "description": "EIP712 revision identifier" }, { "name": "EIP712_DOMAIN", "type": "bytes32", "visibility": "internal", "mutability": "constant", "description": "EIP712 domain typehash" }, { "name": "PERMIT_TYPEHASH", "type": "bytes32", "visibility": "public", "mutability": "constant", "description": "EIP712 permit typehash" }, { "name": "UINT_MAX_VALUE", "type": "uint256", "visibility": "public", "mutability": "constant", "description": "Maximum uint256 value" }, { "name": "ATOKEN_REVISION", "type": "uint256", "visibility": "public", "mutability": "constant", "description": "Contract revision number" }, { "name": "UNDERLYING_ASSET_ADDRESS", "type": "address", "visibility": "public", "mutability": "immutable", "description": "Address of the underlying ERC20 asset" }, { "name": "RESERVE_TREASURY_ADDRESS", "type": "address", "visibility": "public", "mutability": "immutable", "description": "Address of the reserve treasury" }, { "name": "POOL", "type": "ILendingPool", "visibility": "public", "mutability": "immutable", "description": "Reference to the LendingPool contract" }, { "name": "_nonces", "type": "mapping(address => uint256)", "visibility": "public", "mutability": "", "description": "Nonces for EIP712 permit functionality" }, { "name": "DOMAIN_SEPARATOR", "type": "bytes32", "visibility": "public", "mutability": "", "description": "EIP712 domain separator" } ], "functions": [ { "name": "constructor", "signature": "constructor(ILendingPool pool, address underlyingAssetAddress, address reserveTreasuryAddress, string memory tokenName, string memory tokenSymbol, address incentivesController)", "code": "constructor(\n ILendingPool pool,\n address underlyingAssetAddress,\n address reserveTreasuryAddress,\n string memory tokenName,\n string memory tokenSymbol,\n address incentivesController\n) public IncentivizedERC20(tokenName, tokenSymbol, 18, incentivesController) {\n POOL = pool;\n UNDERLYING_ASSET_ADDRESS = underlyingAssetAddress;\n RESERVE_TREASURY_ADDRESS = reserveTreasuryAddress;\n}", "comment": "Initializes the AToken contract with core parameters", "visibility": "public", "modifiers": [], "parameters": [ { "name": "pool", "type": "ILendingPool", "description": "LendingPool contract reference" }, { "name": "underlyingAssetAddress", "type": "address", "description": "Address of the underlying asset" }, { "name": "reserveTreasuryAddress", "type": "address", "description": "Treasury address for protocol fees" }, { "name": "tokenName", "type": "string memory", "description": "Name of the AToken" }, { "name": "tokenSymbol", "type": "string memory", "description": "Symbol of the AToken" }, { "name": "incentivesController", "type": "address", "description": "Address of incentives controller" } ], "returns": "", "output_property": "Sets immutable state variables POOL, UNDERLYING_ASSET_ADDRESS, and RESERVE_TREASURY_ADDRESS. Emits no events. Does not revert under normal conditions.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "getRevision", "signature": "getRevision()", "code": "function getRevision() internal pure virtual override returns (uint256) {\n return ATOKEN_REVISION;\n}", "comment": "Returns the revision number of the contract", "visibility": "internal pure virtual", "modifiers": [], "parameters": [], "returns": "uint256 - ATOKEN_REVISION constant", "output_property": "Always returns ATOKEN_REVISION (0x1). Pure function with no side effects.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "initialize", "signature": "initialize(uint8 underlyingAssetDecimals, string calldata tokenName, string calldata tokenSymbol)", "code": "function initialize(\n uint8 underlyingAssetDecimals,\n string calldata tokenName,\n string calldata tokenSymbol\n) external virtual initializer {\n uint256 chainId;\n\n //solium-disable-next-line\n assembly {\n chainId := chainid()\n }\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\n _setName(tokenName);\n _setSymbol(tokenSymbol);\n _setDecimals(underlyingAssetDecimals);\n}", "comment": "Initializes the contract state after deployment (upgradeable pattern)", "visibility": "external virtual", "modifiers": [ "initializer" ], "parameters": [ { "name": "underlyingAssetDecimals", "type": "uint8", "description": "Decimals of underlying asset" }, { "name": "tokenName", "type": "string calldata", "description": "Name of the token" }, { "name": "tokenSymbol", "type": "string calldata", "description": "Symbol of the token" } ], "returns": "", "output_property": "Sets DOMAIN_SEPARATOR for EIP712 permits, sets token name, symbol, and decimals. Can only be called once due to initializer modifier. Reverts if called more than once.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "burn", "signature": "burn(address user, address receiverOfUnderlying, uint256 amount, uint256 index)", "code": "function burn(\n address user,\n address receiverOfUnderlying,\n uint256 amount,\n uint256 index\n) external override onlyLendingPool {\n uint256 amountScaled = amount.rayDiv(index);\n require(amountScaled != 0, Errors.CT_INVALID_BURN_AMOUNT);\n _burn(user, amountScaled);\n\n IERC20(UNDERLYING_ASSET_ADDRESS).safeTransfer(receiverOfUnderlying, amount);\n\n emit Transfer(user, address(0), amount);\n emit Burn(user, receiverOfUnderlying, amount, index);\n}", "comment": "Burns aTokens from `user` and sends the equivalent amount of underlying to `receiverOfUnderlying`. Only callable by the LendingPool, as extra state updates there need to be managed", "visibility": "external override", "modifiers": [ "onlyLendingPool" ], "parameters": [ { "name": "user", "type": "address", "description": "The owner of the aTokens, getting them burned" }, { "name": "receiverOfUnderlying", "type": "address", "description": "The address that will receive the underlying" }, { "name": "amount", "type": "uint256", "description": "The amount being burned" }, { "name": "index", "type": "uint256", "description": "The new liquidity index of the reserve" } ], "returns": "Returns true if successfully burns x ATokens from the account of u and sends x underlying tokens to t", "output_property": "Burns amountScaled aTokens from user, transfers amount underlying tokens to receiverOfUnderlying. Emits Transfer and Burn events. Reverts if msg.sender is not LendingPool, if amountScaled == 0, or if user has insufficient balance.", "events": [ "Transfer", "Burn" ], "vulnerable": false, "vulnerability_details": { "issue": "Additive burn (rounding vulnerability)", "severity": "Medium", "description": "Due to rounding in conversions to AToken, if the conversion rate is high enough, one can withdraw a small amount that will result in the system transferring underlying tokens but burning zero ATokens of the user's account", "mitigation": "Fixed by adding require(amountScaled != 0) check to prevent zero-value burns" }, "property": "Burning is additive, it can be performed either all at once or in steps.", "property_specification": { "precondition": "User has AToken balance B", "operation": "burn(user, receiver, amount, index)", "postcondition": "User's AToken balance = B - amount (within rounding tolerance ε)", "actual": "When amount.rayDiv(index) rounds down to 0, the burn operation transfers amount underlying tokens but burns 0 ATokens, resulting in user AToken balance unchanged = B, violating the postcondition where the balance should be B - amount." } }, { "name": "mint", "signature": "mint(address user, uint256 amount, uint256 index)", "code": "function mint(\n address user,\n uint256 amount,\n uint256 index\n) external override onlyLendingPool returns (bool) {\n uint256 previousBalance = super.balanceOf(user);\n\n uint256 amountScaled = amount.rayDiv(index);\n require(amountScaled != 0, Errors.CT_INVALID_MINT_AMOUNT);\n _mint(user, amountScaled);\n\n emit Transfer(address(0), user, amount);\n emit Mint(user, amount, index);\n\n return previousBalance == 0;\n}", "comment": "Mints `amount` aTokens to `user`. Only callable by the LendingPool, as extra state updates there need to be managed", "visibility": "external override", "modifiers": [ "onlyLendingPool" ], "parameters": [ { "name": "user", "type": "address", "description": "The address receiving the minted tokens" }, { "name": "amount", "type": "uint256", "description": "The amount of tokens getting minted" }, { "name": "index", "type": "uint256", "description": "The new liquidity index of the reserve" } ], "returns": "bool - true if the previous balance of the user was 0", "output_property": "Mints amountScaled aTokens to user, transfers amount underlying tokens from pool. Returns true if user had zero balance before mint. Emits Transfer and Mint events. Reverts if msg.sender is not LendingPool, if amountScaled == 0, or if amount is zero.", "events": [ "Transfer", "Mint" ], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "mintToTreasury", "signature": "mintToTreasury(uint256 amount, uint256 index)", "code": "function mintToTreasury(uint256 amount, uint256 index) external override onlyLendingPool {\n if (amount == 0) {\n return;\n }\n\n // Compared to the normal mint, we don't check for rounding errors.\n // The amount to mint can easily be very small since it is a fraction of the interest accrued.\n // In that case, the treasury will experience a (very small) loss, but it\n // wont cause potentially valid transactions to fail.\n _mint(RESERVE_TREASURY_ADDRESS, amount.rayDiv(index));\n\n emit Transfer(address(0), RESERVE_TREASURY_ADDRESS, amount);\n emit Mint(RESERVE_TREASURY_ADDRESS, amount, index);\n}", "comment": "Mints aTokens to the reserve treasury. Only callable by the LendingPool", "visibility": "external override", "modifiers": [ "onlyLendingPool" ], "parameters": [ { "name": "amount", "type": "uint256", "description": "The amount of tokens getting minted" }, { "name": "index", "type": "uint256", "description": "The new liquidity index of the reserve" } ], "returns": "", "output_property": "Mints scaled amount to RESERVE_TREASURY_ADDRESS. Does not validate rounding errors by design. Emits Transfer and Mint events. Reverts if msg.sender is not LendingPool.", "events": [ "Transfer", "Mint" ], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "transferOnLiquidation", "signature": "transferOnLiquidation(address from, address to, uint256 value)", "code": "function transferOnLiquidation(\n address from,\n address to,\n uint256 value\n) external override onlyLendingPool {\n // Being a normal transfer, the Transfer() and BalanceTransfer() are emitted\n // so no need to emit a specific event here\n _transfer(from, to, value, false);\n\n emit Transfer(from, to, value);\n}", "comment": "Transfers aTokens in the event of a borrow being liquidated, in case the liquidators reclaims the aToken. Only callable by the LendingPool", "visibility": "external override", "modifiers": [ "onlyLendingPool" ], "parameters": [ { "name": "from", "type": "address", "description": "The address getting liquidated, current owner of the aTokens" }, { "name": "to", "type": "address", "description": "The recipient" }, { "name": "value", "type": "uint256", "description": "The amount of tokens getting transferred" } ], "returns": "", "output_property": "Transfers value aTokens from from to to without validation. Emits Transfer event. Reverts if msg.sender is not LendingPool or if from has insufficient balance.", "events": [ "Transfer", "BalanceTransfer" ], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "balanceOf", "signature": "balanceOf(address user)", "code": "function balanceOf(address user)\n public\n view\n override(IncentivizedERC20, IERC20)\n returns (uint256)\n{\n return super.balanceOf(user).rayMul(POOL.getReserveNormalizedIncome(UNDERLYING_ASSET_ADDRESS));\n}", "comment": "Calculates the balance of the user: principal balance + interest generated by the principal", "visibility": "public view override", "modifiers": [], "parameters": [ { "name": "user", "type": "address", "description": "The user whose balance is calculated" } ], "returns": "uint256 - The balance of the user", "output_property": "Returns scaled balance multiplied by normalized income. View function with no state modifications. Reverts only if underlying calls revert.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "scaledBalanceOf", "signature": "scaledBalanceOf(address user)", "code": "function scaledBalanceOf(address user) external view override returns (uint256) {\n return super.balanceOf(user);\n}", "comment": "Returns the scaled balance of the user. The scaled balance is the sum of all the updated stored balance divided by the reserve's liquidity index at the moment of the update", "visibility": "external view override", "modifiers": [], "parameters": [ { "name": "user", "type": "address", "description": "The user whose balance is calculated" } ], "returns": "uint256 - The scaled balance of the user", "output_property": "Returns raw scaled balance without applying interest. View function with no side effects.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "getScaledUserBalanceAndSupply", "signature": "getScaledUserBalanceAndSupply(address user)", "code": "function getScaledUserBalanceAndSupply(address user)\n external\n view\n override\n returns (uint256, uint256)\n{\n return (super.balanceOf(user), super.totalSupply());\n}", "comment": "Returns the scaled balance of the user and the scaled total supply", "visibility": "external view override", "modifiers": [], "parameters": [ { "name": "user", "type": "address", "description": "The address of the user" } ], "returns": "(uint256, uint256) - The scaled balance of the user and the scaled total supply", "output_property": "Returns tuple of scaled user balance and scaled total supply. View function with no side effects.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "totalSupply", "signature": "totalSupply()", "code": "function totalSupply() public view override(IncentivizedERC20, IERC20) returns (uint256) {\n uint256 currentSupplyScaled = super.totalSupply();\n\n if (currentSupplyScaled == 0) {\n return 0;\n }\n\n return currentSupplyScaled.rayMul(POOL.getReserveNormalizedIncome(UNDERLYING_ASSET_ADDRESS));\n}", "comment": "Calculates the total supply of the specific aToken since the balance of every single user increases over time, the total supply does that too", "visibility": "public view override", "modifiers": [], "parameters": [], "returns": "uint256 - The current total supply", "output_property": "Returns scaled total supply multiplied by normalized income. View function with no side effects. Returns 0 if scaled supply is 0.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "scaledTotalSupply", "signature": "scaledTotalSupply()", "code": "function scaledTotalSupply() public view virtual override returns (uint256) {\n return super.totalSupply();\n}", "comment": "Returns the scaled total supply of the variable debt token. Represents sum(debt/index)", "visibility": "public view virtual override", "modifiers": [], "parameters": [], "returns": "uint256 - The scaled total supply", "output_property": "Returns raw scaled total supply without applying interest. View function with no side effects.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "transferUnderlyingTo", "signature": "transferUnderlyingTo(address target, uint256 amount)", "code": "function transferUnderlyingTo(address target, uint256 amount)\n external\n override\n onlyLendingPool\n returns (uint256)\n{\n IERC20(UNDERLYING_ASSET_ADDRESS).safeTransfer(target, amount);\n return amount;\n}", "comment": "Transfers the underlying asset to `target`. Used by the LendingPool to transfer assets in borrow(), withdraw() and flashLoan()", "visibility": "external override", "modifiers": [ "onlyLendingPool" ], "parameters": [ { "name": "target", "type": "address", "description": "The recipient of the aTokens" }, { "name": "amount", "type": "uint256", "description": "The amount getting transferred" } ], "returns": "uint256 - The amount transferred", "output_property": "Transfers amount underlying tokens to target. Returns amount transferred. Reverts if msg.sender is not LendingPool or if transfer fails.", "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)", "code": "function permit(\n address owner,\n address spender,\n uint256 value,\n uint256 deadline,\n uint8 v,\n bytes32 r,\n bytes32 s\n) external {\n require(owner != address(0), 'INVALID_OWNER');\n //solium-disable-next-line\n require(block.timestamp <= deadline, 'INVALID_EXPIRATION');\n uint256 currentValidNonce = _nonces[owner];\n bytes32 digest =\n 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": "Implements the permit function as per EIP-2612", "visibility": "external", "modifiers": [], "parameters": [ { "name": "owner", "type": "address", "description": "The owner of the funds" }, { "name": "spender", "type": "address", "description": "The spender" }, { "name": "value", "type": "uint256", "description": "The amount" }, { "name": "deadline", "type": "uint256", "description": "The deadline timestamp, type(uint256).max for max deadline" }, { "name": "v", "type": "uint8", "description": "Signature param" }, { "name": "r", "type": "bytes32", "description": "Signature param" }, { "name": "s", "type": "bytes32", "description": "Signature param" } ], "returns": "", "output_property": "Approves spender to spend value on behalf of owner via EIP-2612 permit. Increments nonce. Reverts if owner is zero address, deadline passed, or signature invalid.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "_transfer (4 params)", "signature": "_transfer(address from, address to, uint256 amount, bool validate)", "code": "function _transfer(\n address from,\n address to,\n uint256 amount,\n bool validate\n) internal {\n uint256 index = POOL.getReserveNormalizedIncome(UNDERLYING_ASSET_ADDRESS);\n\n uint256 fromBalanceBefore = super.balanceOf(from).rayMul(index);\n uint256 toBalanceBefore = super.balanceOf(to).rayMul(index);\n\n super._transfer(from, to, amount.rayDiv(index));\n\n if (validate) {\n POOL.finalizeTransfer(\n UNDERLYING_ASSET_ADDRESS,\n from,\n to,\n amount,\n fromBalanceBefore,\n toBalanceBefore\n );\n }\n\n emit BalanceTransfer(from, to, amount, index);\n}", "comment": "Transfers the aTokens between two users. Validates the transfer (ie checks for valid HF after the transfer) if required", "visibility": "internal", "modifiers": [], "parameters": [ { "name": "from", "type": "address", "description": "The source address" }, { "name": "to", "type": "address", "description": "The destination address" }, { "name": "amount", "type": "uint256", "description": "The amount getting transferred" }, { "name": "validate", "type": "bool", "description": "true if the transfer needs to be validated" } ], "returns": "", "output_property": "Transfers amount aTokens from from to to after converting to scaled amount. Emits BalanceTransfer event. Calls POOL.finalizeTransfer if validate is true. Reverts if from has insufficient balance.", "events": [ "BalanceTransfer" ], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "_transfer (override)", "signature": "_transfer(address from, address to, uint256 amount)", "code": "function _transfer(\n address from,\n address to,\n uint256 amount\n) internal override {\n _transfer(from, to, amount, true);\n}", "comment": "Overrides the parent _transfer to force validated transfer() and transferFrom()", "visibility": "internal override", "modifiers": [], "parameters": [ { "name": "from", "type": "address", "description": "The source address" }, { "name": "to", "type": "address", "description": "The destination address" }, { "name": "amount", "type": "uint256", "description": "The amount getting transferred" } ], "returns": "", "output_property": "Calls internal _transfer with validate=true, ensuring health factor check for standard transfers.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null } ], "modifiers": [ { "name": "onlyLendingPool", "definition": "require(_msgSender() == address(POOL), Errors.CT_CALLER_MUST_BE_LENDING_POOL);", "purpose": "Restricts function calls to only the LendingPool contract" } ], "inheritance": [ "VersionedInitializable", "IncentivizedERC20", "IAToken" ], "call_graph": { "constructor": [ "IncentivizedERC20.constructor()" ], "initialize": [ "_setName()", "_setSymbol()", "_setDecimals()" ], "burn": [ "amount.rayDiv(index)", "_burn()", "IERC20.safeTransfer()", "emit Transfer()", "emit Burn()" ], "mint": [ "super.balanceOf()", "amount.rayDiv(index)", "_mint()", "emit Transfer()", "emit Mint()" ], "mintToTreasury": [ "_mint()", "emit Transfer()", "emit Mint()" ], "transferOnLiquidation": [ "_transfer() (4 params)", "emit Transfer()" ], "balanceOf": [ "super.balanceOf()", "POOL.getReserveNormalizedIncome()", "rayMul()" ], "totalSupply": [ "super.totalSupply()", "POOL.getReserveNormalizedIncome()", "rayMul()" ], "transferUnderlyingTo": [ "IERC20.safeTransfer()" ], "permit": [ "ecrecover()", "_nonces[owner].add()", "_approve()" ], "_transfer (4 params)": [ "POOL.getReserveNormalizedIncome()", "super.balanceOf()", "rayMul()", "super._transfer()", "amount.rayDiv()", "POOL.finalizeTransfer()", "emit BalanceTransfer()" ], "_transfer (override)": [ "_transfer() (4 params)" ] }, "audit_issues": [ { "function": "burn", "issue": "Additive burn (AToken)", "severity": "Medium", "description": "Due to rounding in conversions to AToken, if the conversion rate is high enough, one can withdraw a small amount that will result in the system transferring underlying tokens but burning zero ATokens of the user's account. This results in system loss of assets and user gain of assets.", "status": "Fixed", "mitigation": "Added require(amountScaled != 0, Errors.CT_INVALID_BURN_AMOUNT) to prevent zero-value burns", "property": "When a user burns x amount of ATokens, the user's AToken balance should decrease by x, and the user should receive x amount of underlying tokens. Due to rounding errors in conversion, a user could receive underlying tokens without burning any ATokens, breaking the invariant that burning decreases AToken balance proportionally to the underlying amount transferred.", "property_specification": "precondition: User has AToken balance B. operation: burn(user, receiver, amount, index). postcondition: User's AToken balance = B - amount (within rounding tolerance ε). actual vulnerability: When amount.rayDiv(index) rounds down to 0, the burn operation transfers amount underlying tokens but burns 0 ATokens, resulting in user AToken balance unchanged = B, violating the postcondition where the balance should be B - amount." } ], "events": [ { "name": "Transfer", "parameters": "address indexed from, address indexed to, uint256 amount", "description": "Emitted when tokens are transferred (inherited from ERC20)" }, { "name": "Burn", "parameters": "address user, address receiverOfUnderlying, uint256 amount, uint256 index", "description": "Emitted when aTokens are burned and underlying tokens are withdrawn" }, { "name": "Mint", "parameters": "address user, uint256 amount, uint256 index", "description": "Emitted when aTokens are minted" }, { "name": "BalanceTransfer", "parameters": "address from, address to, uint256 amount, uint256 index", "description": "Emitted during transfers to track balance changes with interest index" } ] }, { "contract_name": "StableDebtToken", "file_name": "StableDebtToken.txt", "file_path": "https://github.com/aave/protocol-v2/blob/750920303e33b66bc29862ea3b85206dda9ce786/contracts/protocol/tokenization/StableDebtToken.sol", "metadata": { "license": "agpl-3.0", "solidity_version": "0.6.12", "description": "Implements a stable debt token to track the borrowing positions of users at stable rate mode", "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": "internal", "mutability": "", "description": "Average stable rate across all stable rate debt positions" }, { "name": "_timestamps", "type": "mapping(address => uint40)", "visibility": "internal", "mutability": "", "description": "Timestamp of the last user action for each user" }, { "name": "_usersStableRate", "type": "mapping(address => uint256)", "visibility": "internal", "mutability": "", "description": "Stable rate for each user" }, { "name": "_totalSupplyTimestamp", "type": "uint40", "visibility": "internal", "mutability": "", "description": "Timestamp at which the total supply was last updated" } ], "functions": [ { "name": "constructor", "signature": "constructor(address pool, address underlyingAsset, string memory name, string memory symbol, address incentivesController)", "code": "constructor(\n address pool,\n address underlyingAsset,\n string memory name,\n string memory symbol,\n address incentivesController\n) public DebtTokenBase(pool, underlyingAsset, name, symbol, incentivesController) {}", "comment": "Initializes the StableDebtToken contract with core parameters", "visibility": "public", "modifiers": [], "parameters": [ { "name": "pool", "type": "address", "description": "Address of the LendingPool" }, { "name": "underlyingAsset", "type": "address", "description": "Address of the underlying asset" }, { "name": "name", "type": "string memory", "description": "Name of the debt token" }, { "name": "symbol", "type": "string memory", "description": "Symbol of the debt token" }, { "name": "incentivesController", "type": "address", "description": "Address of the incentives controller" } ], "returns": "", "output_property": "Passes parameters to DebtTokenBase constructor. No additional state changes. Does not revert under normal conditions.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "getRevision", "signature": "getRevision()", "code": "function getRevision() internal pure virtual override returns (uint256) {\n return DEBT_TOKEN_REVISION;\n}", "comment": "Gets the revision of the stable debt token implementation", "visibility": "internal pure virtual", "modifiers": [], "parameters": [], "returns": "uint256 - The debt token implementation revision", "output_property": "Always returns DEBT_TOKEN_REVISION (0x1). Pure function with no side effects.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "getAverageStableRate", "signature": "getAverageStableRate()", "code": "function getAverageStableRate() external view virtual override returns (uint256) {\n return _avgStableRate;\n}", "comment": "Returns the average stable rate across all the stable rate debt", "visibility": "external view virtual", "modifiers": [], "parameters": [], "returns": "uint256 - The average stable rate", "output_property": "Returns current _avgStableRate value. View function with no side effects.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "getUserLastUpdated", "signature": "getUserLastUpdated(address user)", "code": "function getUserLastUpdated(address user) external view virtual override returns (uint40) {\n return _timestamps[user];\n}", "comment": "Returns the timestamp of the last user action", "visibility": "external view virtual", "modifiers": [], "parameters": [ { "name": "user", "type": "address", "description": "The address of the user" } ], "returns": "uint40 - The last update timestamp", "output_property": "Returns timestamp of last user interaction. View function with no side effects.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "getUserStableRate", "signature": "getUserStableRate(address user)", "code": "function getUserStableRate(address user) external view virtual override returns (uint256) {\n return _usersStableRate[user];\n}", "comment": "Returns the stable rate of the user", "visibility": "external view virtual", "modifiers": [], "parameters": [ { "name": "user", "type": "address", "description": "The address of the user" } ], "returns": "uint256 - The stable rate of user", "output_property": "Returns stable rate for the user. View function with no side effects.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "balanceOf", "signature": "balanceOf(address account)", "code": "function balanceOf(address account) public view virtual override returns (uint256) {\n uint256 accountBalance = super.balanceOf(account);\n uint256 stableRate = _usersStableRate[account];\n if (accountBalance == 0) {\n return 0;\n }\n uint256 cumulatedInterest =\n MathUtils.calculateCompoundedInterest(stableRate, _timestamps[account]);\n return accountBalance.rayMul(cumulatedInterest);\n}", "comment": "Calculates the current user debt balance", "visibility": "public view virtual", "modifiers": [], "parameters": [ { "name": "account", "type": "address", "description": "The address of the user" } ], "returns": "uint256 - The accumulated debt of the user", "output_property": "Returns user debt including accrued interest. View function. Returns 0 if principal balance is 0.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "mint", "signature": "mint(address user, address onBehalfOf, uint256 amount, uint256 rate)", "code": "function mint(\n address user,\n address onBehalfOf,\n uint256 amount,\n uint256 rate\n) external override onlyLendingPool returns (bool) {\n MintLocalVars memory vars;\n\n if (user != onBehalfOf) {\n _decreaseBorrowAllowance(onBehalfOf, user, amount);\n }\n\n (, uint256 currentBalance, uint256 balanceIncrease) = _calculateBalanceIncrease(onBehalfOf);\n\n vars.previousSupply = totalSupply();\n vars.currentAvgStableRate = _avgStableRate;\n vars.nextSupply = _totalSupply = vars.previousSupply.add(amount);\n\n vars.amountInRay = amount.wadToRay();\n\n vars.newStableRate = _usersStableRate[onBehalfOf]\n .rayMul(currentBalance.wadToRay())\n .add(vars.amountInRay.rayMul(rate))\n .rayDiv(currentBalance.add(amount).wadToRay());\n\n require(vars.newStableRate <= type(uint128).max, Errors.SDT_STABLE_DEBT_OVERFLOW);\n _usersStableRate[onBehalfOf] = vars.newStableRate;\n\n //solium-disable-next-line\n _totalSupplyTimestamp = _timestamps[onBehalfOf] = uint40(block.timestamp);\n\n // Calculates the updated average stable rate\n vars.currentAvgStableRate = _avgStableRate = vars\n .currentAvgStableRate\n .rayMul(vars.previousSupply.wadToRay())\n .add(rate.rayMul(vars.amountInRay))\n .rayDiv(vars.nextSupply.wadToRay());\n\n _mint(onBehalfOf, amount.add(balanceIncrease), vars.previousSupply);\n\n emit Transfer(address(0), onBehalfOf, amount);\n\n emit Mint(\n user,\n onBehalfOf,\n amount,\n currentBalance,\n balanceIncrease,\n vars.newStableRate,\n vars.currentAvgStableRate,\n vars.nextSupply\n );\n\n return currentBalance == 0;\n}", "comment": "Mints debt token to the `onBehalfOf` address. Only callable by the LendingPool. The resulting rate is the weighted average between the rate of the new debt and the rate of the previous debt", "visibility": "external override", "modifiers": [ "onlyLendingPool" ], "parameters": [ { "name": "user", "type": "address", "description": "The address receiving the borrowed underlying, being the delegatee in case of credit delegate, or same as `onBehalfOf` otherwise" }, { "name": "onBehalfOf", "type": "address", "description": "The address receiving the debt tokens" }, { "name": "amount", "type": "uint256", "description": "The amount of debt tokens to mint" }, { "name": "rate", "type": "uint256", "description": "The rate of the debt being minted" } ], "returns": "bool - true if the previous balance of the user was 0", "output_property": "Mints stable debt tokens to onBehalfOf, updates user stable rate and average stable rate. Returns true if user had zero balance before mint. Emits Transfer and Mint events. Reverts if not called by LendingPool, if newStableRate exceeds uint128 max, or if amount is zero.", "events": [ "Transfer", "Mint" ], "vulnerable": false, "vulnerability_details": { "issue": "Additive mint (Stable debt token)", "severity": "Medium", "description": "Due to rounding in conversions to stable debt token, if the conversion rate is high enough, one can deposit a small amount that will result in the system transferring underlying tokens but minting debt tokens to the user's account", "mitigation": "Fixed to avoid transfer on mint/burn of zero stable debt tokens" }, "property": "Minting is additive, it can be performed either all at once or in steps", "property_specification": { "precondition": "User has debt balance B", "operation": "mint(user, onBehalfOf, amount, rate)", "postcondition": "User's debt balance = B + amount (within rounding tolerance ε)", "actual": "When amount conversion rounds down to 0 in intermediate calculations, the mint operation may mint zero debt tokens while still transferring underlying tokens (or vice versa), resulting in user debt balance unchanged = B, violating the postcondition where the balance should be B + amount." } }, { "name": "burn", "signature": "burn(address user, uint256 amount)", "code": "function burn(address user, uint256 amount) external override onlyLendingPool {\n (, uint256 currentBalance, uint256 balanceIncrease) = _calculateBalanceIncrease(user);\n\n uint256 previousSupply = totalSupply();\n uint256 newAvgStableRate = 0;\n uint256 nextSupply = 0;\n uint256 userStableRate = _usersStableRate[user];\n\n // Since the total supply and each single user debt accrue separately,\n // there might be accumulation errors so that the last borrower repaying\n // mght actually try to repay more than the available debt supply.\n // In this case we simply set the total supply and the avg stable rate to 0\n if (previousSupply <= amount) {\n _avgStableRate = 0;\n _totalSupply = 0;\n } else {\n nextSupply = _totalSupply = previousSupply.sub(amount);\n uint256 firstTerm = _avgStableRate.rayMul(previousSupply.wadToRay());\n uint256 secondTerm = userStableRate.rayMul(amount.wadToRay());\n\n // For the same reason described above, when the last user is repaying it might\n // happen that user rate * user balance > avg rate * total supply. In that case,\n // we simply set the avg rate to 0\n if (secondTerm >= firstTerm) {\n newAvgStableRate = _avgStableRate = _totalSupply = 0;\n } else {\n newAvgStableRate = _avgStableRate = firstTerm.sub(secondTerm).rayDiv(nextSupply.wadToRay());\n }\n }\n\n if (amount == currentBalance) {\n _usersStableRate[user] = 0;\n _timestamps[user] = 0;\n } else {\n //solium-disable-next-line\n _timestamps[user] = uint40(block.timestamp);\n }\n //solium-disable-next-line\n _totalSupplyTimestamp = uint40(block.timestamp);\n\n if (balanceIncrease > amount) {\n uint256 amountToMint = balanceIncrease.sub(amount);\n _mint(user, amountToMint, previousSupply);\n emit Mint(\n user,\n user,\n amountToMint,\n currentBalance,\n balanceIncrease,\n userStableRate,\n newAvgStableRate,\n nextSupply\n );\n } else {\n uint256 amountToBurn = amount.sub(balanceIncrease);\n _burn(user, amountToBurn, previousSupply);\n emit Burn(user, amountToBurn, currentBalance, balanceIncrease, newAvgStableRate, nextSupply);\n }\n\n emit Transfer(user, address(0), amount);\n}", "comment": "Burns debt of `user`. Only callable by the LendingPool", "visibility": "external override", "modifiers": [ "onlyLendingPool" ], "parameters": [ { "name": "user", "type": "address", "description": "The address of the user getting his debt burned" }, { "name": "amount", "type": "uint256", "description": "The amount of debt tokens getting burned" } ], "returns": "", "output_property": "Burns stable debt tokens from user, updates user state and average stable rate. Handles edge cases where repayment exceeds supply. Emits Transfer and either Mint or Burn events. Reverts if not called by LendingPool or if user has insufficient balance.", "events": [ "Transfer", "Mint", "Burn" ], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "_calculateBalanceIncrease", "signature": "_calculateBalanceIncrease(address user)", "code": "function _calculateBalanceIncrease(address user)\n internal\n view\n returns (\n uint256,\n uint256,\n uint256\n )\n{\n uint256 previousPrincipalBalance = super.balanceOf(user);\n\n if (previousPrincipalBalance == 0) {\n return (0, 0, 0);\n }\n\n // Calculation of the accrued interest since the last accumulation\n uint256 balanceIncrease = balanceOf(user).sub(previousPrincipalBalance);\n\n return (\n previousPrincipalBalance,\n previousPrincipalBalance.add(balanceIncrease),\n balanceIncrease\n );\n}", "comment": "Calculates the increase in balance since the last user interaction", "visibility": "internal view", "modifiers": [], "parameters": [ { "name": "user", "type": "address", "description": "The address of the user for which the interest is being accumulated" } ], "returns": "(uint256, uint256, uint256) - (previous principal balance, new principal balance, balance increase)", "output_property": "Returns principal balance, updated balance with interest, and the increase amount. View function. Returns zeros if user has no balance.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "getSupplyData", "signature": "getSupplyData()", "code": "function getSupplyData()\n public\n view\n override\n returns (\n uint256,\n uint256,\n uint256,\n uint40\n )\n{\n uint256 avgRate = _avgStableRate;\n return (super.totalSupply(), _calcTotalSupply(avgRate), avgRate, _totalSupplyTimestamp);\n}", "comment": "Returns the principal and total supply, the average borrow rate and the last supply update timestamp", "visibility": "public view override", "modifiers": [], "parameters": [], "returns": "(uint256, uint256, uint256, uint40) - (principal total supply, total supply with interest, average rate, last update timestamp)", "output_property": "Returns supply data including principal total supply, total supply with interest, average rate, and last update timestamp. View function.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "getTotalSupplyAndAvgRate", "signature": "getTotalSupplyAndAvgRate()", "code": "function getTotalSupplyAndAvgRate() public view override returns (uint256, uint256) {\n uint256 avgRate = _avgStableRate;\n return (_calcTotalSupply(avgRate), avgRate);\n}", "comment": "Returns the total supply and the average stable rate", "visibility": "public view override", "modifiers": [], "parameters": [], "returns": "(uint256, uint256) - (total supply with interest, average rate)", "output_property": "Returns total supply with accrued interest and average stable rate. View function.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "totalSupply", "signature": "totalSupply()", "code": "function totalSupply() public view override returns (uint256) {\n return _calcTotalSupply(_avgStableRate);\n}", "comment": "Returns the total supply", "visibility": "public view override", "modifiers": [], "parameters": [], "returns": "uint256 - Total supply with accrued interest", "output_property": "Returns total supply including accrued interest. View function.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "getTotalSupplyLastUpdated", "signature": "getTotalSupplyLastUpdated()", "code": "function getTotalSupplyLastUpdated() public view override returns (uint40) {\n return _totalSupplyTimestamp;\n}", "comment": "Returns the timestamp at which the total supply was updated", "visibility": "public view override", "modifiers": [], "parameters": [], "returns": "uint40 - The last update timestamp", "output_property": "Returns timestamp of last total supply update. View function.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "principalBalanceOf", "signature": "principalBalanceOf(address user)", "code": "function principalBalanceOf(address user) external view virtual override returns (uint256) {\n return super.balanceOf(user);\n}", "comment": "Returns the principal debt balance of the user", "visibility": "external view virtual", "modifiers": [], "parameters": [ { "name": "user", "type": "address", "description": "The user's address" } ], "returns": "uint256 - The debt balance of the user since the last burn/mint action", "output_property": "Returns principal debt balance without accrued interest. View function.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "_calcTotalSupply", "signature": "_calcTotalSupply(uint256 avgRate)", "code": "function _calcTotalSupply(uint256 avgRate) internal view virtual returns (uint256) {\n uint256 principalSupply = super.totalSupply();\n\n if (principalSupply == 0) {\n return 0;\n }\n\n uint256 cumulatedInterest =\n MathUtils.calculateCompoundedInterest(avgRate, _totalSupplyTimestamp);\n\n return principalSupply.rayMul(cumulatedInterest);\n}", "comment": "Calculates the total supply with accrued interest", "visibility": "internal view virtual", "modifiers": [], "parameters": [ { "name": "avgRate", "type": "uint256", "description": "The average rate at which the total supply increases" } ], "returns": "uint256 - The total supply with accrued interest", "output_property": "Returns total supply including compounded interest. View function. Returns 0 if principal supply is 0.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "_mint", "signature": "_mint(address account, uint256 amount, uint256 oldTotalSupply)", "code": "function _mint(\n address account,\n uint256 amount,\n uint256 oldTotalSupply\n) internal {\n uint256 oldAccountBalance = _balances[account];\n _balances[account] = oldAccountBalance.add(amount);\n\n if (address(_incentivesController) != address(0)) {\n _incentivesController.handleAction(account, oldTotalSupply, oldAccountBalance);\n }\n}", "comment": "Mints stable debt tokens to a user", "visibility": "internal", "modifiers": [], "parameters": [ { "name": "account", "type": "address", "description": "The account receiving the debt tokens" }, { "name": "amount", "type": "uint256", "description": "The amount being minted" }, { "name": "oldTotalSupply", "type": "uint256", "description": "The total supply before the minting event" } ], "returns": "", "output_property": "Increases account balance by amount. Notifies incentives controller if set. Does not revert unless addition overflows.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "_burn", "signature": "_burn(address account, uint256 amount, uint256 oldTotalSupply)", "code": "function _burn(\n address account,\n uint256 amount,\n uint256 oldTotalSupply\n) internal {\n uint256 oldAccountBalance = _balances[account];\n _balances[account] = oldAccountBalance.sub(amount, Errors.SDT_BURN_EXCEEDS_BALANCE);\n\n if (address(_incentivesController) != address(0)) {\n _incentivesController.handleAction(account, oldTotalSupply, oldAccountBalance);\n }\n}", "comment": "Burns stable debt tokens of a user", "visibility": "internal", "modifiers": [], "parameters": [ { "name": "account", "type": "address", "description": "The user getting his debt burned" }, { "name": "amount", "type": "uint256", "description": "The amount being burned" }, { "name": "oldTotalSupply", "type": "uint256", "description": "The total supply before the burning event" } ], "returns": "", "output_property": "Decreases account balance by amount. Reverts if amount exceeds balance. Notifies incentives controller if set.", "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 the mint function to avoid stack too deep errors" } ], "modifiers": [ { "name": "onlyLendingPool", "definition": "Inherited from DebtTokenBase", "purpose": "Restricts function calls to only the LendingPool contract" } ], "inheritance": [ "IStableDebtToken", "DebtTokenBase" ], "call_graph": { "constructor": [ "DebtTokenBase.constructor()" ], "mint": [ "_decreaseBorrowAllowance()", "_calculateBalanceIncrease()", "totalSupply()", "wadToRay()", "rayMul()", "rayDiv()", "_mint()", "emit Transfer()", "emit Mint()" ], "burn": [ "_calculateBalanceIncrease()", "totalSupply()", "wadToRay()", "rayMul()", "sub()", "rayDiv()", "_mint()", "_burn()", "emit Transfer()", "emit Mint()", "emit Burn()" ], "balanceOf": [ "super.balanceOf()", "MathUtils.calculateCompoundedInterest()", "rayMul()" ], "getSupplyData": [ "_calcTotalSupply()" ], "getTotalSupplyAndAvgRate": [ "_calcTotalSupply()" ], "totalSupply": [ "_calcTotalSupply()" ], "_calcTotalSupply": [ "super.totalSupply()", "MathUtils.calculateCompoundedInterest()", "rayMul()" ], "_calculateBalanceIncrease": [ "super.balanceOf()", "balanceOf()", "sub()", "add()" ], "_mint": [ "_balances[account].add()", "_incentivesController.handleAction()" ], "_burn": [ "_balances[account].sub()", "_incentivesController.handleAction()" ] }, "audit_issues": [ { "function": "mint", "issue": "Additive mint (Stable debt token)", "severity": "Medium", "description": "Due to rounding in conversions to stable debt token, if the conversion rate is high enough, one can deposit a small amount that will result in the system transferring underlying tokens but minting debt tokens to the user's account. This results in system loss of assets and user gain of assets.", "status": "Fixed", "mitigation": "Fixed to avoid transfer on mint/burn of zero stable debt tokens", "property": "When a user mints x amount of stable debt tokens, the user's debt balance should increase by x, and the protocol should receive x amount of underlying tokens from the user. Due to rounding errors in conversion, a user could receive debt tokens without transferring underlying tokens, or could transfer underlying tokens without receiving debt tokens, breaking the invariant that minting increases debt balance proportionally to underlying amount transferred.", "property_specification": "precondition: User has debt balance B. operation: mint(user, onBehalfOf, amount, rate). postcondition: User's debt balance = B + amount (within rounding tolerance ε). actual vulnerability: When amount conversion rounds down to 0 in intermediate calculations, the mint operation may mint zero debt tokens while still transferring underlying tokens (or vice versa), resulting in user debt balance unchanged = B, violating the postcondition where the balance should be B + amount." } ], "events": [ { "name": "Transfer", "parameters": "address indexed from, address indexed to, uint256 amount", "description": "Emitted when tokens are transferred (inherited from ERC20)" }, { "name": "Mint", "parameters": "address user, address onBehalfOf, uint256 amount, uint256 currentBalance, uint256 balanceIncrease, uint256 newStableRate, uint256 avgStableRate, uint256 newTotalSupply", "description": "Emitted when stable debt tokens are minted" }, { "name": "Burn", "parameters": "address user, uint256 amount, uint256 currentBalance, uint256 balanceIncrease, uint256 avgStableRate, uint256 newTotalSupply", "description": "Emitted when stable debt tokens are burned" } ] }, { "contract_name": "ATokenVault", "file_name": "ATokenVault.sol", "file_path": "https://github.com/aave/aave-vault/blob/23366cc1188cf901585cf487e811e97fd712e6e5/src/ATokenVault.sol", "metadata": { "license": "UNLICENSED", "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": "The Aave v3 Pool Addresses Provider." }, { "name": "AAVE_POOL", "type": "IPool", "visibility": "public", "mutability": "immutable", "description": "The Aave v3 Pool." }, { "name": "ATOKEN", "type": "IAToken", "visibility": "public", "mutability": "immutable", "description": "The aToken associated with the underlying asset." }, { "name": "UNDERLYING", "type": "IERC20Upgradeable", "visibility": "public", "mutability": "immutable", "description": "The underlying ERC20 asset which can be supplied to Aave." }, { "name": "REFERRAL_CODE", "type": "uint16", "visibility": "public", "mutability": "immutable", "description": "The Aave referral code to use for deposits from this vault." }, { "name": "_s", "type": "VaultState", "visibility": "internal", "mutability": "", "description": "The storage struct for the vault's internal state, including lastVaultBalance, accumulatedFees, and fee." }, { "name": "_sigNonces", "type": "mapping(address => uint256)", "visibility": "internal", "mutability": "", "description": "Nonces used for replay protection on meta-transactions." } ], "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\n address aTokenAddress = AAVE_POOL.getReserveData(address(underlying)).aTokenAddress;\n require(aTokenAddress != address(0), \"ASSET_NOT_SUPPORTED\");\n ATOKEN = IAToken(aTokenAddress);\n}", "comment": "Constructor that sets immutable variables and initializes the aToken address.", "visibility": "public", "modifiers": [], "parameters": [ { "name": "underlying", "type": "address", "description": "The underlying ERC20 asset." }, { "name": "referralCode", "type": "uint16", "description": "The Aave referral code." }, { "name": "poolAddressesProvider", "type": "IPoolAddressesProvider", "description": "The address of the Aave v3 Pool Addresses Provider." } ], "returns": "", "output_property": "Initializes immutable state variables and disables initializers for the upgradeable pattern. Reverts if the underlying asset is not supported by Aave (i.e., aToken address is zero).", "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)", "code": "function initialize(\n address owner,\n uint256 initialFee,\n string memory shareName,\n string memory shareSymbol,\n uint256 initialLockDeposit\n) external initializer {\n require(owner != address(0), \"ZERO_ADDRESS_NOT_VALID\");\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\n UNDERLYING.safeApprove(address(AAVE_POOL), type(uint256).max);\n\n _handleDeposit(initialLockDeposit, address(this), msg.sender, false);\n}", "comment": "Initializes the vault, setting the initial parameters and initializing inherited contracts. It requires an initial non-zero deposit to prevent a frontrunning attack.", "visibility": "external", "modifiers": [ "initializer" ], "parameters": [ { "name": "owner", "type": "address", "description": "The owner to set." }, { "name": "initialFee", "type": "uint256", "description": "The initial fee to set, expressed in wad, where 1e18 is 100%." }, { "name": "shareName", "type": "string", "description": "The name to set for this vault." }, { "name": "shareSymbol", "type": "string", "description": "The symbol to set for this vault." }, { "name": "initialLockDeposit", "type": "uint256", "description": "The initial amount of underlying assets to deposit." } ], "returns": "", "output_property": "Initializes the vault by setting the owner, fee, and making the first deposit. Reverts if owner is zero address or initial deposit is zero.", "events": [ "FeeUpdated", "Deposit", "YieldAccrued" ], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "deposit", "signature": "deposit(uint256 assets, address receiver)", "code": "function deposit(uint256 assets, address receiver) public override(ERC4626Upgradeable, IATokenVault) returns (uint256) {\n return _handleDeposit(assets, receiver, msg.sender, false);\n}", "comment": "Deposits underlying assets into the vault.", "visibility": "public", "modifiers": [], "parameters": [ { "name": "assets", "type": "uint256", "description": "Amount of underlying assets to deposit." }, { "name": "receiver", "type": "address", "description": "Address to receive vault shares." } ], "returns": "uint256 - Amount of shares minted.", "output_property": "Calls _handleDeposit to deposit underlying assets, accrues yield, and mints shares to receiver. Reverts if deposit exceeds maxDeposit or if rounding results in zero shares.", "events": [ "Deposit", "YieldAccrued" ], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "depositATokens", "signature": "depositATokens(uint256 assets, address receiver)", "code": "function depositATokens(uint256 assets, address receiver) public override returns (uint256) {\n return _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": "Address to receive vault shares." } ], "returns": "uint256 - Amount of shares minted.", "output_property": "Calls _handleDeposit to deposit aTokens, accrues yield, and mints shares to receiver. Reverts if rounding results in zero shares.", "events": [ "Deposit", "YieldAccrued" ], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "depositWithSig", "signature": "depositWithSig(uint256 assets, address receiver, address depositor, EIP712Signature calldata sig)", "code": "function depositWithSig(\n uint256 assets,\n address receiver,\n address depositor,\n EIP712Signature calldata sig\n) public override returns (uint256) {\n unchecked {\n MetaTxHelpers._validateRecoveredAddress(\n MetaTxHelpers._calculateDigest(\n keccak256(\n abi.encode(\n DEPOSIT_WITH_SIG_TYPEHASH,\n assets,\n receiver,\n depositor,\n _sigNonces[depositor]++,\n sig.deadline\n )\n ),\n _domainSeparatorV4()\n ),\n depositor,\n sig\n );\n }\n return _handleDeposit(assets, receiver, depositor, false);\n}", "comment": "Meta-transaction version of deposit.", "visibility": "public", "modifiers": [], "parameters": [ { "name": "assets", "type": "uint256", "description": "Amount of underlying assets to deposit." }, { "name": "receiver", "type": "address", "description": "Address to receive vault shares." }, { "name": "depositor", "type": "address", "description": "Address of the depositor (signer)." }, { "name": "sig", "type": "EIP712Signature", "description": "EIP-712 signature data." } ], "returns": "uint256 - Amount of shares minted.", "output_property": "Validates EIP-712 signature, then deposits underlying assets. Reverts if signature is invalid or expired.", "events": [ "Deposit", "YieldAccrued" ], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "depositATokensWithSig", "signature": "depositATokensWithSig(uint256 assets, address receiver, address depositor, EIP712Signature calldata sig)", "code": "function depositATokensWithSig(\n uint256 assets,\n address receiver,\n address depositor,\n EIP712Signature calldata sig\n) public override returns (uint256) {\n unchecked {\n MetaTxHelpers._validateRecoveredAddress(\n MetaTxHelpers._calculateDigest(\n keccak256(\n abi.encode(\n DEPOSIT_ATOKENS_WITH_SIG_TYPEHASH,\n assets,\n receiver,\n depositor,\n _sigNonces[depositor]++,\n sig.deadline\n )\n ),\n _domainSeparatorV4()\n ),\n depositor,\n sig\n );\n }\n return _handleDeposit(assets, receiver, depositor, true);\n}", "comment": "Meta-transaction version of depositATokens.", "visibility": "public", "modifiers": [], "parameters": [ { "name": "assets", "type": "uint256", "description": "Amount of aTokens to deposit." }, { "name": "receiver", "type": "address", "description": "Address to receive vault shares." }, { "name": "depositor", "type": "address", "description": "Address of the depositor (signer)." }, { "name": "sig", "type": "EIP712Signature", "description": "EIP-712 signature data." } ], "returns": "uint256 - Amount of shares minted.", "output_property": "Validates EIP-712 signature, then deposits aTokens. Reverts if signature is invalid or expired.", "events": [ "Deposit", "YieldAccrued" ], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "mint", "signature": "mint(uint256 shares, address receiver)", "code": "function mint(uint256 shares, address receiver) public override(ERC4626Upgradeable, IATokenVault) returns (uint256) {\n return _handleMint(shares, receiver, msg.sender, false);\n}", "comment": "Mints a specified number of 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": "Address to receive vault shares." } ], "returns": "uint256 - Amount of assets deposited.", "output_property": "Calls _handleMint to mint shares by depositing underlying assets. Reverts if mint exceeds maxMint.", "events": [ "Deposit", "YieldAccrued" ], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "mintWithATokens", "signature": "mintWithATokens(uint256 shares, address receiver)", "code": "function mintWithATokens(uint256 shares, address receiver) public override returns (uint256) {\n return _handleMint(shares, receiver, msg.sender, true);\n}", "comment": "Mints a specified number of vault shares by depositing aTokens.", "visibility": "public", "modifiers": [], "parameters": [ { "name": "shares", "type": "uint256", "description": "Amount of shares to mint." }, { "name": "receiver", "type": "address", "description": "Address to receive vault shares." } ], "returns": "uint256 - Amount of assets deposited.", "output_property": "Calls _handleMint to mint shares by depositing aTokens.", "events": [ "Deposit", "YieldAccrued" ], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "mintWithSig", "signature": "mintWithSig(uint256 shares, address receiver, address depositor, EIP712Signature calldata sig)", "code": "function mintWithSig(\n uint256 shares,\n address receiver,\n address depositor,\n EIP712Signature calldata sig\n) public override returns (uint256) {\n unchecked {\n MetaTxHelpers._validateRecoveredAddress(\n MetaTxHelpers._calculateDigest(\n keccak256(\n abi.encode(MINT_WITH_SIG_TYPEHASH, shares, receiver, depositor, _sigNonces[depositor]++, sig.deadline)\n ),\n _domainSeparatorV4()\n ),\n depositor,\n sig\n );\n }\n return _handleMint(shares, receiver, depositor, false);\n}", "comment": "Meta-transaction version of mint.", "visibility": "public", "modifiers": [], "parameters": [ { "name": "shares", "type": "uint256", "description": "Amount of shares to mint." }, { "name": "receiver", "type": "address", "description": "Address to receive vault shares." }, { "name": "depositor", "type": "address", "description": "Address of the depositor (signer)." }, { "name": "sig", "type": "EIP712Signature", "description": "EIP-712 signature data." } ], "returns": "uint256 - Amount of assets deposited.", "output_property": "Validates EIP-712 signature, then mints shares. Reverts if signature is invalid or expired.", "events": [ "Deposit", "YieldAccrued" ], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "mintWithATokensWithSig", "signature": "mintWithATokensWithSig(uint256 shares, address receiver, address depositor, EIP712Signature calldata sig)", "code": "function mintWithATokensWithSig(\n uint256 shares,\n address receiver,\n address depositor,\n EIP712Signature calldata sig\n) public override returns (uint256) {\n unchecked {\n MetaTxHelpers._validateRecoveredAddress(\n MetaTxHelpers._calculateDigest(\n keccak256(\n abi.encode(\n MINT_WITH_ATOKENS_WITH_SIG_TYPEHASH,\n shares,\n receiver,\n depositor,\n _sigNonces[depositor]++,\n sig.deadline\n )\n ),\n _domainSeparatorV4()\n ),\n depositor,\n sig\n );\n }\n return _handleMint(shares, receiver, depositor, true);\n}", "comment": "Meta-transaction version of mintWithATokens.", "visibility": "public", "modifiers": [], "parameters": [ { "name": "shares", "type": "uint256", "description": "Amount of shares to mint." }, { "name": "receiver", "type": "address", "description": "Address to receive vault shares." }, { "name": "depositor", "type": "address", "description": "Address of the depositor (signer)." }, { "name": "sig", "type": "EIP712Signature", "description": "EIP-712 signature data." } ], "returns": "uint256 - Amount of assets deposited.", "output_property": "Validates EIP-712 signature, then mints shares by depositing aTokens. Reverts if signature is invalid or expired.", "events": [ "Deposit", "YieldAccrued" ], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "withdraw", "signature": "withdraw(uint256 assets, address receiver, address owner)", "code": "function withdraw(\n uint256 assets,\n address receiver,\n address owner\n) public override(ERC4626Upgradeable, IATokenVault) returns (uint256) {\n return _handleWithdraw(assets, receiver, owner, msg.sender, false);\n}", "comment": "Withdraws underlying assets from the vault.", "visibility": "public", "modifiers": [], "parameters": [ { "name": "assets", "type": "uint256", "description": "Amount of underlying assets to withdraw." }, { "name": "receiver", "type": "address", "description": "Address to receive the withdrawn assets." }, { "name": "owner", "type": "address", "description": "Address of the vault share owner." } ], "returns": "uint256 - Amount of shares burned.", "output_property": "Calls _handleWithdraw to withdraw underlying assets, accrues yield, and burns shares from owner. Reverts if withdrawal exceeds maxWithdraw.", "events": [ "Withdraw", "YieldAccrued" ], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "withdrawATokens", "signature": "withdrawATokens(uint256 assets, address receiver, address owner)", "code": "function withdrawATokens(uint256 assets, address receiver, address owner) public override returns (uint256) {\n return _handleWithdraw(assets, receiver, owner, msg.sender, true);\n}", "comment": "Withdraws aTokens directly from the vault.", "visibility": "public", "modifiers": [], "parameters": [ { "name": "assets", "type": "uint256", "description": "Amount of aTokens to withdraw." }, { "name": "receiver", "type": "address", "description": "Address to receive the withdrawn aTokens." }, { "name": "owner", "type": "address", "description": "Address of the vault share owner." } ], "returns": "uint256 - Amount of shares burned.", "output_property": "Calls _handleWithdraw to withdraw aTokens, accrues yield, and burns shares from owner.", "events": [ "Withdraw", "YieldAccrued" ], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "withdrawWithSig", "signature": "withdrawWithSig(uint256 assets, address receiver, address owner, EIP712Signature calldata sig)", "code": "function withdrawWithSig(\n uint256 assets,\n address receiver,\n address owner,\n EIP712Signature calldata sig\n) public override returns (uint256) {\n unchecked {\n MetaTxHelpers._validateRecoveredAddress(\n MetaTxHelpers._calculateDigest(\n keccak256(abi.encode(WITHDRAW_WITH_SIG_TYPEHASH, assets, receiver, owner, _sigNonces[owner]++, sig.deadline)),\n _domainSeparatorV4()\n ),\n owner,\n sig\n );\n }\n return _handleWithdraw(assets, receiver, owner, owner, false);\n}", "comment": "Meta-transaction version of withdraw.", "visibility": "public", "modifiers": [], "parameters": [ { "name": "assets", "type": "uint256", "description": "Amount of underlying assets to withdraw." }, { "name": "receiver", "type": "address", "description": "Address to receive the withdrawn assets." }, { "name": "owner", "type": "address", "description": "Address of the vault share owner." }, { "name": "sig", "type": "EIP712Signature", "description": "EIP-712 signature data." } ], "returns": "uint256 - Amount of shares burned.", "output_property": "Validates EIP-712 signature, then withdraws underlying assets. Reverts if signature is invalid or expired.", "events": [ "Withdraw", "YieldAccrued" ], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "withdrawATokensWithSig", "signature": "withdrawATokensWithSig(uint256 assets, address receiver, address owner, EIP712Signature calldata sig)", "code": "function withdrawATokensWithSig(\n uint256 assets,\n address receiver,\n address owner,\n EIP712Signature calldata sig\n) public override returns (uint256) {\n unchecked {\n MetaTxHelpers._validateRecoveredAddress(\n MetaTxHelpers._calculateDigest(\n keccak256(\n abi.encode(\n WITHDRAW_ATOKENS_WITH_SIG_TYPEHASH,\n assets,\n receiver,\n owner,\n _sigNonces[owner]++,\n sig.deadline\n )\n ),\n _domainSeparatorV4()\n ),\n owner,\n sig\n );\n }\n return _handleWithdraw(assets, receiver, owner, owner, true);\n}", "comment": "Meta-transaction version of withdrawATokens.", "visibility": "public", "modifiers": [], "parameters": [ { "name": "assets", "type": "uint256", "description": "Amount of aTokens to withdraw." }, { "name": "receiver", "type": "address", "description": "Address to receive the withdrawn aTokens." }, { "name": "owner", "type": "address", "description": "Address of the vault share owner." }, { "name": "sig", "type": "EIP712Signature", "description": "EIP-712 signature data." } ], "returns": "uint256 - Amount of shares burned.", "output_property": "Validates EIP-712 signature, then withdraws aTokens. Reverts if signature is invalid or expired.", "events": [ "Withdraw", "YieldAccrued" ], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "redeem", "signature": "redeem(uint256 shares, address receiver, address owner)", "code": "function redeem(\n uint256 shares,\n address receiver,\n address owner\n) public override(ERC4626Upgradeable, IATokenVault) returns (uint256) {\n return _handleRedeem(shares, receiver, owner, msg.sender, false);\n}", "comment": "Redeems a specified number of vault shares for underlying assets.", "visibility": "public", "modifiers": [], "parameters": [ { "name": "shares", "type": "uint256", "description": "Amount of shares to redeem." }, { "name": "receiver", "type": "address", "description": "Address to receive the withdrawn assets." }, { "name": "owner", "type": "address", "description": "Address of the vault share owner." } ], "returns": "uint256 - Amount of assets withdrawn.", "output_property": "Calls _handleRedeem to redeem shares for underlying assets, accrues yield, and burns shares from owner. Reverts if redemption exceeds maxRedeem or if rounding results in zero assets.", "events": [ "Withdraw", "YieldAccrued" ], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "redeemAsATokens", "signature": "redeemAsATokens(uint256 shares, address receiver, address owner)", "code": "function redeemAsATokens(uint256 shares, address receiver, address owner) public override returns (uint256) {\n return _handleRedeem(shares, receiver, owner, msg.sender, true);\n}", "comment": "Redeems a specified number of vault shares for aTokens.", "visibility": "public", "modifiers": [], "parameters": [ { "name": "shares", "type": "uint256", "description": "Amount of shares to redeem." }, { "name": "receiver", "type": "address", "description": "Address to receive the withdrawn aTokens." }, { "name": "owner", "type": "address", "description": "Address of the vault share owner." } ], "returns": "uint256 - Amount of aTokens withdrawn.", "output_property": "Calls _handleRedeem to redeem shares for aTokens, accrues yield, and burns shares from owner.", "events": [ "Withdraw", "YieldAccrued" ], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "redeemWithSig", "signature": "redeemWithSig(uint256 shares, address receiver, address owner, EIP712Signature calldata sig)", "code": "function redeemWithSig(\n uint256 shares,\n address receiver,\n address owner,\n EIP712Signature calldata sig\n) public override returns (uint256) {\n unchecked {\n MetaTxHelpers._validateRecoveredAddress(\n MetaTxHelpers._calculateDigest(\n keccak256(abi.encode(REDEEM_WITH_SIG_TYPEHASH, shares, receiver, owner, _sigNonces[owner]++, sig.deadline)),\n _domainSeparatorV4()\n ),\n owner,\n sig\n );\n }\n return _handleRedeem(shares, receiver, owner, owner, false);\n}", "comment": "Meta-transaction version of redeem.", "visibility": "public", "modifiers": [], "parameters": [ { "name": "shares", "type": "uint256", "description": "Amount of shares to redeem." }, { "name": "receiver", "type": "address", "description": "Address to receive the withdrawn assets." }, { "name": "owner", "type": "address", "description": "Address of the vault share owner." }, { "name": "sig", "type": "EIP712Signature", "description": "EIP-712 signature data." } ], "returns": "uint256 - Amount of assets withdrawn.", "output_property": "Validates EIP-712 signature, then redeems shares for underlying assets. Reverts if signature is invalid or expired.", "events": [ "Withdraw", "YieldAccrued" ], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "redeemWithATokensWithSig", "signature": "redeemWithATokensWithSig(uint256 shares, address receiver, address owner, EIP712Signature calldata sig)", "code": "function redeemWithATokensWithSig(\n uint256 shares,\n address receiver,\n address owner,\n EIP712Signature calldata sig\n) public override returns (uint256) {\n unchecked {\n MetaTxHelpers._validateRecoveredAddress(\n MetaTxHelpers._calculateDigest(\n keccak256(\n abi.encode(\n REDEEM_WITH_ATOKENS_WITH_SIG_TYPEHASH,\n shares,\n receiver,\n owner,\n _sigNonces[owner]++,\n sig.deadline\n )\n ),\n _domainSeparatorV4()\n ),\n owner,\n sig\n );\n }\n return _handleRedeem(shares, receiver, owner, owner, true);\n}", "comment": "Meta-transaction version of redeemAsATokens.", "visibility": "public", "modifiers": [], "parameters": [ { "name": "shares", "type": "uint256", "description": "Amount of shares to redeem." }, { "name": "receiver", "type": "address", "description": "Address to receive the withdrawn aTokens." }, { "name": "owner", "type": "address", "description": "Address of the vault share owner." }, { "name": "sig", "type": "EIP712Signature", "description": "EIP-712 signature data." } ], "returns": "uint256 - Amount of aTokens withdrawn.", "output_property": "Validates EIP-712 signature, then redeems shares for aTokens. Reverts if signature is invalid or expired.", "events": [ "Withdraw", "YieldAccrued" ], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "maxDeposit", "signature": "maxDeposit(address)", "code": "function maxDeposit(address) public view override(ERC4626Upgradeable, IATokenVault) returns (uint256) {\n return _maxAssetsSuppliableToAave();\n}", "comment": "Returns the maximum amount of underlying assets that can be deposited.", "visibility": "public", "modifiers": [], "parameters": [ { "name": "", "type": "address", "description": "Address of the receiver (unused)." } ], "returns": "uint256 - Maximum deposit amount.", "output_property": "Returns the maximum amount of underlying assets that can be supplied to Aave, based on supply caps and reserve status. Reverts if reserve data cannot be fetched.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "maxMint", "signature": "maxMint(address)", "code": "function maxMint(address) public view override(ERC4626Upgradeable, IATokenVault) returns (uint256) {\n return _convertToShares(_maxAssetsSuppliableToAave(), MathUpgradeable.Rounding.Down);\n}", "comment": "Returns the maximum number of shares that can be minted.", "visibility": "public", "modifiers": [], "parameters": [ { "name": "", "type": "address", "description": "Address of the receiver (unused)." } ], "returns": "uint256 - Maximum mint amount in shares.", "output_property": "Calculates and returns the maximum number of shares that can be minted, based on the max supply to Aave.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "maxWithdraw", "signature": "maxWithdraw(address owner)", "code": "function maxWithdraw(address owner) public view override(ERC4626Upgradeable, IATokenVault) returns (uint256) {\n uint256 maxWithdrawable = _maxAssetsWithdrawableFromAave();\n return\n maxWithdrawable == 0 ? 0 : maxWithdrawable.min(_convertToAssets(balanceOf(owner), MathUpgradeable.Rounding.Down));\n}", "comment": "Returns the maximum amount of underlying assets that can be withdrawn by a user.", "visibility": "public", "modifiers": [], "parameters": [ { "name": "owner", "type": "address", "description": "Address of the share owner." } ], "returns": "uint256 - Maximum withdrawable amount in underlying assets.", "output_property": "Returns the minimum of the owner's assets value and the available liquidity in Aave.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "maxRedeem", "signature": "maxRedeem(address owner)", "code": "function maxRedeem(address owner) public view override(ERC4626Upgradeable, IATokenVault) returns (uint256) {\n uint256 maxWithdrawable = _maxAssetsWithdrawableFromAave();\n return\n maxWithdrawable == 0 ? 0 : _convertToShares(maxWithdrawable, MathUpgradeable.Rounding.Down).min(balanceOf(owner));\n}", "comment": "Returns the maximum number of shares that can be redeemed by a user.", "visibility": "public", "modifiers": [], "parameters": [ { "name": "owner", "type": "address", "description": "Address of the share owner." } ], "returns": "uint256 - Maximum redeemable shares.", "output_property": "Returns the minimum of the owner's share balance and the maximum redeemable shares based on Aave's liquidity.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "previewDeposit", "signature": "previewDeposit(uint256 assets)", "code": "function previewDeposit(uint256 assets) public view override(ERC4626Upgradeable, IATokenVault) returns (uint256) {\n return _convertToShares(_maxAssetsSuppliableToAave().min(assets), MathUpgradeable.Rounding.Down);\n}", "comment": "Previews the number of shares that would be minted for a given deposit.", "visibility": "public", "modifiers": [], "parameters": [ { "name": "assets", "type": "uint256", "description": "Amount of underlying assets to deposit." } ], "returns": "uint256 - Amount of shares that would be minted.", "output_property": "Calculates shares based on the minimum of the deposit amount and the max suppliable to Aave. The EIP-4626 standard requires this function not to account for deposit limits; however, this implementation does, violating that specification.", "events": [], "vulnerable": false, "vulnerability_details": { "issue": "Non-compliance with EIP4626 standard - previewDeposit", "severity": "Informational", "description": "As per EIP4626, all the preview functions must not take into account any limitation of the system, like those returned by the max() methods. In the contract, the preview methods do take into account system limitations.", "mitigation": "As per EIP4626, all the preview functions may revert due to other conditions that would also cause primary functions to revert. Relying on Aave is acceptable, given that primary functions are impacted by its limitations (e.g. users cannot withdraw if there is no available liquidity in the Aave Pool)." }, "property": "MUST return as close to, and no more than, the exact amount of Vault shares that would be minted in a deposit() call in the same transaction", "property_specification": { "precondition": "The vault has a maximum deposit limit L", "operation": "A user calls previewDeposit(x) for an amount x > L", "postcondition": "previewDeposit(x) returns the same number of shares as it would for x = L (since the function should not account for limits)", "actual": "previewDeposit(x) returns the number of shares for L (due to min(x, L)), which is different from what it would return for an amount x' where x' > L, implying the function accounts for the limit, violating the EIP." } }, { "name": "previewMint", "signature": "previewMint(uint256 shares)", "code": "function previewMint(uint256 shares) public view override(ERC4626Upgradeable, IATokenVault) returns (uint256) {\n return _convertToAssets(shares, MathUpgradeable.Rounding.Up).min(_maxAssetsSuppliableToAave());\n}", "comment": "Previews the amount of underlying assets needed to mint a given number of shares.", "visibility": "public", "modifiers": [], "parameters": [ { "name": "shares", "type": "uint256", "description": "Amount of shares to mint." } ], "returns": "uint256 - Amount of underlying assets required.", "output_property": "Calculates the required assets based on the shares and caps it by the maximum suppliable amount to Aave. This violates the EIP-4626 standard which requires preview functions not to account for such limits.", "events": [], "vulnerable": false, "vulnerability_details": { "issue": "Non-compliance with EIP4626 standard - previewMint", "severity": "Informational", "description": "As per EIP4626, all the preview functions must not take into account any limitation of the system, like those returned by the max() methods. In the contract, the preview methods do take into account system limitations.", "mitigation": "As per EIP4626, all the preview functions may revert due to other conditions that would also cause primary functions to revert. Relying on Aave is acceptable, given that primary functions are impacted by its limitations (e.g. users cannot withdraw if there is no available liquidity in the Aave Pool)." }, "property": "MUST return as close to, and no more than, the exact amount of Vault shares that would be minted in a deposit() call in the same transaction", "property_specification": { "precondition": "The vault has a maximum deposit limit", "operation": "A user calls previewMint(y) for a number of shares y that would require more than L in assets", "postcondition": "previewMint(y) returns the amount of assets needed to mint y shares without accounting for limits", "actual": "previewMint(y) returns the assets for y shares but capped at L, implying the function accounts for the deposit limit, violating the EIP." } }, { "name": "previewWithdraw", "signature": "previewWithdraw(uint256 assets)", "code": "function previewWithdraw(uint256 assets) public view override(ERC4626Upgradeable, IATokenVault) returns (uint256) {\n uint256 maxWithdrawable = _maxAssetsWithdrawableFromAave();\n return maxWithdrawable == 0 ? 0 : _convertToShares(maxWithdrawable.min(assets), MathUpgradeable.Rounding.Up);\n}", "comment": "Previews the number of shares that would be burned to withdraw a given amount of assets.", "visibility": "public", "modifiers": [], "parameters": [ { "name": "assets", "type": "uint256", "description": "Amount of assets to withdraw." } ], "returns": "uint256 - Amount of shares that would be burned.", "output_property": "Calculates shares based on the minimum of the withdrawal amount and the available liquidity in Aave. This violates the EIP-4626 standard which requires preview functions not to account for such limits.", "events": [], "vulnerable": false, "vulnerability_details": { "issue": "Non-compliance with EIP4626 standard - previewWithdraw", "severity": "Informational", "description": "As per EIP4626, all the preview functions must not take into account any limitation of the system, like those returned by the max() methods. In the contract, the preview methods do take into account system limitations.", "mitigation": "As per EIP4626, all the preview functions may revert due to other conditions that would also cause primary functions to revert. Relying on Aave is acceptable, given that primary functions are impacted by its limitations (e.g. users cannot withdraw if there is no available liquidity in the Aave Pool)." }, "property": "previewWithdraw() MUST NOT account for withdrawal limits like those returned from maxWithdraw()", "property_specification": { "precondition": "The vault has available liquidity", "operation": "A user calls previewWithdraw(x) for an amount x > L", "postcondition": "previewWithdraw(x) returns the number of shares needed to withdraw x assets without accounting for limits", "actual": "previewWithdraw(x) returns the number of shares for L (due to min(x, L)), implying the function accounts for the withdrawal limit, violating the EIP-4626 standard" } }, { "name": "previewRedeem", "signature": "previewRedeem(uint256 shares)", "code": "function previewRedeem(uint256 shares) public view override(ERC4626Upgradeable, IATokenVault) returns (uint256) {\n uint256 maxWithdrawable = _maxAssetsWithdrawableFromAave();\n return maxWithdrawable == 0 ? 0 : _convertToAssets(shares, MathUpgradeable.Rounding.Down).min(maxWithdrawable);\n}", "comment": "Previews the amount of assets that would be received for redeeming a given number of shares.", "visibility": "public", "modifiers": [], "parameters": [ { "name": "shares", "type": "uint256", "description": "Amount of shares to redeem." } ], "returns": "uint256 - Amount of assets that would be received.", "output_property": "Calculates the assets based on the shares and caps it by the available liquidity in Aave. This violates the EIP-4626 standard which requires preview functions not to account for such limits.", "events": [], "vulnerable": false, "vulnerability_details": { "issue": "Non-compliance with EIP4626 standard - previewRedeem", "severity": "Informational", "description": "As per EIP4626, all the preview functions must not take into account any limitation of the system, like those returned by the max() methods. In the contract, the preview methods do take into account system limitations.", "mitigation": "As per EIP4626, all the preview functions may revert due to other conditions that would also cause primary functions to revert. Relying on Aave is acceptable, given that primary functions are impacted by its limitations (e.g. users cannot withdraw if there is no available liquidity in the Aave Pool)." }, "property": "previewWithdraw() MUST NOT account for withdrawal limits like those returned from maxWithdraw()", "property_specification": { "precondition": "The vault has available liquidity", "operation": "A user calls previewRedeem(y) for a number of shares y that would require more than L in assets", "postcondition": "previewRedeem(y) returns the amount of assets that would be received for y shares without accounting for limits", "actual": "previewRedeem(y) returns the assets for y shares but capped at L, implying the function accounts for the withdrawal limit, violating the EIP-4626 standard" } }, { "name": "domainSeparator", "signature": "domainSeparator()", "code": "function domainSeparator() public view override returns (bytes32) {\n return _domainSeparatorV4();\n}", "comment": "Returns the EIP-712 domain separator for meta-transactions.", "visibility": "public", "modifiers": [], "parameters": [], "returns": "bytes32 - The domain separator.", "output_property": "Returns the domain separator used for EIP-712 signatures.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "setFee", "signature": "setFee(uint256 newFee)", "code": "function setFee(uint256 newFee) public override onlyOwner {\n _accrueYield();\n _setFee(newFee);\n}", "comment": "Sets a new fee percentage for the vault.", "visibility": "public", "modifiers": [ "onlyOwner" ], "parameters": [ { "name": "newFee", "type": "uint256", "description": "The new fee, expressed in wad where 1e18 is 100%." } ], "returns": "", "output_property": "Accrues yield, then updates the fee. Reverts if the new fee exceeds SCALE (1e18).", "events": [ "FeeUpdated", "YieldAccrued" ], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "withdrawFees", "signature": "withdrawFees(address to, uint256 amount)", "code": "function withdrawFees(address to, uint256 amount) public override onlyOwner {\n _accrueYield();\n require(amount <= _s.accumulatedFees, \"INSUFFICIENT_FEES\"); // will underflow below anyway, error msg for clarity\n\n _s.accumulatedFees -= uint128(amount);\n\n ATOKEN.transfer(to, amount);\n\n _s.lastVaultBalance = uint128(ATOKEN.balanceOf(address(this)));\n\n emit FeesWithdrawn(to, amount, _s.lastVaultBalance, _s.accumulatedFees);\n}", "comment": "Withdraws accumulated fees.", "visibility": "public", "modifiers": [ "onlyOwner" ], "parameters": [ { "name": "to", "type": "address", "description": "Address to receive the fees." }, { "name": "amount", "type": "uint256", "description": "Amount of fees to withdraw." } ], "returns": "", "output_property": "Accrues yield, then transfers the specified amount of aTokens to the recipient. Reverts if the fee amount is insufficient.", "events": [ "FeesWithdrawn", "YieldAccrued" ], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "claimRewards", "signature": "claimRewards(address to)", "code": "function claimRewards(address to) public override onlyOwner {\n require(to != address(0), \"CANNOT_CLAIM_TO_ZERO_ADDRESS\");\n\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\n emit RewardsClaimed(to, rewardsList, claimedAmounts);\n}", "comment": "Claims rewards from Aave for the vault's aToken holdings.", "visibility": "public", "modifiers": [ "onlyOwner" ], "parameters": [ { "name": "to", "type": "address", "description": "Address to receive the claimed rewards." } ], "returns": "", "output_property": "Claims all rewards for the vault's aToken from Aave's rewards controller and sends them to the specified address. Reverts if the address is zero.", "events": [ "RewardsClaimed" ], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "emergencyRescue", "signature": "emergencyRescue(address token, address to, uint256 amount)", "code": "function emergencyRescue(address token, address to, uint256 amount) public override onlyOwner {\n require(token != address(ATOKEN), \"CANNOT_RESCUE_ATOKEN\");\n\n IERC20Upgradeable(token).safeTransfer(to, amount);\n\n emit EmergencyRescue(token, to, amount);\n}", "comment": "Allows the owner to rescue any ERC20 token sent to the vault, except the vault's aToken.", "visibility": "public", "modifiers": [ "onlyOwner" ], "parameters": [ { "name": "token", "type": "address", "description": "Address of the token to rescue." }, { "name": "to", "type": "address", "description": "Address to receive the rescued tokens." }, { "name": "amount", "type": "uint256", "description": "Amount of tokens to rescue." } ], "returns": "", "output_property": "Transfers the specified amount of a non-aToken ERC20 token to the recipient. Reverts if trying to rescue the vault's aToken.", "events": [ "EmergencyRescue" ], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "totalAssets", "signature": "totalAssets()", "code": "function totalAssets() public view override(ERC4626Upgradeable, IATokenVault) returns (uint256) {\n // Report only the total assets net of fees, for vault share logic\n return ATOKEN.balanceOf(address(this)) - getClaimableFees();\n}", "comment": "Returns the total assets held by the vault, net of accrued fees.", "visibility": "public", "modifiers": [], "parameters": [], "returns": "uint256 - Total assets.", "output_property": "Returns the vault's aToken balance minus claimable fees. May revert due to arithmetic underflow if getClaimableFees() exceeds the aToken balance. This violates the EIP-4626 requirement that totalAssets must not revert.", "events": [], "vulnerable": false, "vulnerability_details": { "issue": "Non-compliance with EIP4626 standard - non-reverting functions", "severity": "Informational", "description": "As per EIP4626, the functions totalAssets, maxDeposit, maxMint, maxWithdraw, and maxRedeem must not revert by any means. In the contract, however, these functions may revert due to over/underflows of arithmetical computations.", "mitigation": "Although conforming to a standard is important and even essential to a degree, there is likely little to be gained from modifying and altering the core code's functionality to adapt to the minutiae of the standard. As the vault relies inherently on the Aave Protocol, it is acceptable to revert due to Aave-specific calculations." }, "property": "totalAssets must never revert", "property_specification": { "precondition": "The vault's state has claimable fees greater than its aToken balance", "operation": "A user calls totalAssets()", "postcondition": "The function returns a value without reverting", "actual": "The function attempts to compute ATOKEN.balanceOf(address(this)) - getClaimableFees(), which underflows and reverts, violating the EIP-4626 standard" } }, { "name": "getClaimableFees", "signature": "getClaimableFees()", "code": "function getClaimableFees() public view override returns (uint256) {\n uint256 newVaultBalance = ATOKEN.balanceOf(address(this));\n\n // Skip computation if there is no yield\n if (newVaultBalance <= _s.lastVaultBalance) {\n return _s.accumulatedFees;\n }\n\n uint256 newYield = newVaultBalance - _s.lastVaultBalance;\n uint256 newFees = newYield.mulDiv(_s.fee, SCALE, MathUpgradeable.Rounding.Down);\n\n return _s.accumulatedFees + newFees;\n}", "comment": "Returns the total fees that are claimable.", "visibility": "public", "modifiers": [], "parameters": [], "returns": "uint256 - Claimable fees.", "output_property": "Calculates claimable fees as the sum of accumulated fees and a percentage of new yield since the last accrual.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "getSigNonce", "signature": "getSigNonce(address signer)", "code": "function getSigNonce(address signer) public view override returns (uint256) {\n return _sigNonces[signer];\n}", "comment": "Returns the current nonce for a given signer.", "visibility": "public", "modifiers": [], "parameters": [ { "name": "signer", "type": "address", "description": "Address of the signer." } ], "returns": "uint256 - Nonce value.", "output_property": "Returns the current nonce for the specified signer for meta-transaction replay protection.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "getLastVaultBalance", "signature": "getLastVaultBalance()", "code": "function getLastVaultBalance() public view override returns (uint256) {\n return _s.lastVaultBalance;\n}", "comment": "Returns the last recorded vault balance.", "visibility": "public", "modifiers": [], "parameters": [], "returns": "uint256 - Last vault balance.", "output_property": "Returns the stored lastVaultBalance, which is used for fee calculations.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "getFee", "signature": "getFee()", "code": "function getFee() public view override returns (uint256) {\n return _s.fee;\n}", "comment": "Returns the current fee percentage.", "visibility": "public", "modifiers": [], "parameters": [], "returns": "uint256 - Current fee.", "output_property": "Returns the current fee percentage stored in the vault's state.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "_setFee", "signature": "_setFee(uint256 newFee)", "code": "function _setFee(uint256 newFee) internal {\n require(newFee <= SCALE, \"FEE_TOO_HIGH\");\n\n uint256 oldFee = _s.fee;\n _s.fee = uint64(newFee);\n\n emit FeeUpdated(oldFee, newFee);\n}", "comment": "Internal function to set the fee.", "visibility": "internal", "modifiers": [], "parameters": [ { "name": "newFee", "type": "uint256", "description": "The new fee." } ], "returns": "", "output_property": "Updates the fee if it's within the allowed range. Reverts if the fee is too high.", "events": [ "FeeUpdated" ], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "_accrueYield", "signature": "_accrueYield()", "code": "function _accrueYield() internal {\n uint256 newVaultBalance = ATOKEN.balanceOf(address(this));\n\n // Skip computation if there is no yield\n if (newVaultBalance <= _s.lastVaultBalance) {\n return;\n }\n\n uint256 newYield = newVaultBalance - _s.lastVaultBalance;\n uint256 newFeesEarned = newYield.mulDiv(_s.fee, SCALE, MathUpgradeable.Rounding.Down);\n\n _s.accumulatedFees += uint128(newFeesEarned);\n _s.lastVaultBalance = uint128(newVaultBalance);\n\n emit YieldAccrued(newYield, newFeesEarned, newVaultBalance);\n}", "comment": "Accrues yield and updates fees and the last vault balance.", "visibility": "internal", "modifiers": [], "parameters": [], "returns": "", "output_property": "Calculates new yield since last accrual, adds the corresponding fee to accumulatedFees, and updates lastVaultBalance. Emits YieldAccrued event.", "events": [ "YieldAccrued" ], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "_handleDeposit", "signature": "_handleDeposit(uint256 assets, address receiver, address depositor, bool asAToken)", "code": "function _handleDeposit(uint256 assets, address receiver, address depositor, bool asAToken) internal returns (uint256) {\n if (!asAToken) require(assets <= maxDeposit(receiver), \"DEPOSIT_EXCEEDS_MAX\");\n _accrueYield();\n uint256 shares = super.previewDeposit(assets);\n require(shares != 0, \"ZERO_SHARES\"); // Check for rounding error since we round down in previewDeposit.\n _baseDeposit(_convertToAssets(shares, MathUpgradeable.Rounding.Up), shares, depositor, receiver, asAToken);\n return shares;\n}", "comment": "Handles the deposit logic for both underlying and aToken deposits.", "visibility": "internal", "modifiers": [], "parameters": [ { "name": "assets", "type": "uint256", "description": "Amount to deposit." }, { "name": "receiver", "type": "address", "description": "Address to receive shares." }, { "name": "depositor", "type": "address", "description": "Address that is depositing the assets." }, { "name": "asAToken", "type": "bool", "description": "If true, assets are aTokens; otherwise, they are underlying." } ], "returns": "uint256 - Shares minted.", "output_property": "Accrues yield, computes shares, and calls _baseDeposit. Reverts if deposit exceeds maxDeposit (for underlying) or if shares are zero.", "events": [ "Deposit", "YieldAccrued" ], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "_handleMint", "signature": "_handleMint(uint256 shares, address receiver, address depositor, bool asAToken)", "code": "function _handleMint(uint256 shares, address receiver, address depositor, bool asAToken) internal returns (uint256) {\n if (!asAToken) require(shares <= maxMint(receiver), \"MINT_EXCEEDS_MAX\");\n _accrueYield();\n uint256 assets = super.previewMint(shares); // No need to check for rounding error, previewMint rounds up.\n _baseDeposit(assets, shares, depositor, receiver, asAToken);\n return assets;\n}", "comment": "Handles the mint logic for both underlying and aToken deposits.", "visibility": "internal", "modifiers": [], "parameters": [ { "name": "shares", "type": "uint256", "description": "Amount of shares to mint." }, { "name": "receiver", "type": "address", "description": "Address to receive shares." }, { "name": "depositor", "type": "address", "description": "Address that is depositing the assets." }, { "name": "asAToken", "type": "bool", "description": "If true, assets are aTokens; otherwise, they are underlying." } ], "returns": "uint256 - Assets deposited.", "output_property": "Accrues yield, computes assets required, and calls _baseDeposit. Reverts if mint exceeds maxMint (for underlying).", "events": [ "Deposit", "YieldAccrued" ], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "_handleWithdraw", "signature": "_handleWithdraw(uint256 assets, address receiver, address owner, address allowanceTarget, bool asAToken)", "code": "function _handleWithdraw(\n uint256 assets,\n address receiver,\n address owner,\n address allowanceTarget,\n bool asAToken\n) internal returns (uint256) {\n _accrueYield();\n require(assets <= maxWithdraw(owner), \"WITHDRAW_EXCEEDS_MAX\");\n uint256 shares = super.previewWithdraw(assets); // No need to check for rounding error, previewWithdraw rounds up.\n _baseWithdraw(assets, shares, owner, receiver, allowanceTarget, asAToken);\n return shares;\n}", "comment": "Handles the withdrawal logic for both underlying and aToken withdrawals.", "visibility": "internal", "modifiers": [], "parameters": [ { "name": "assets", "type": "uint256", "description": "Amount of assets to withdraw." }, { "name": "receiver", "type": "address", "description": "Address to receive the assets." }, { "name": "owner", "type": "address", "description": "Address of the share owner." }, { "name": "allowanceTarget", "type": "address", "description": "Address that is authorized to spend the shares (if different from owner)." }, { "name": "asAToken", "type": "bool", "description": "If true, withdraw aTokens; otherwise, withdraw underlying." } ], "returns": "uint256 - Shares burned.", "output_property": "Accrues yield, computes shares to burn, and calls _baseWithdraw. Reverts if withdrawal exceeds maxWithdraw.", "events": [ "Withdraw", "YieldAccrued" ], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "_handleRedeem", "signature": "_handleRedeem(uint256 shares, address receiver, address owner, address allowanceTarget, bool asAToken)", "code": "function _handleRedeem(\n uint256 shares,\n address receiver,\n address owner,\n address allowanceTarget,\n bool asAToken\n) internal returns (uint256) {\n _accrueYield();\n require(shares <= maxRedeem(owner), \"REDEEM_EXCEEDS_MAX\");\n uint256 assets = super.previewRedeem(shares);\n require(assets != 0, \"ZERO_ASSETS\"); // Check for rounding error since we round down in previewRedeem.\n _baseWithdraw(assets, shares, owner, receiver, allowanceTarget, asAToken);\n return assets;\n}", "comment": "Handles the redeem logic for both underlying and aToken withdrawals.", "visibility": "internal", "modifiers": [], "parameters": [ { "name": "shares", "type": "uint256", "description": "Amount of shares to redeem." }, { "name": "receiver", "type": "address", "description": "Address to receive the assets." }, { "name": "owner", "type": "address", "description": "Address of the share owner." }, { "name": "allowanceTarget", "type": "address", "description": "Address that is authorized to spend the shares (if different from owner)." }, { "name": "asAToken", "type": "bool", "description": "If true, withdraw aTokens; otherwise, withdraw underlying." } ], "returns": "uint256 - Assets withdrawn.", "output_property": "Accrues yield, computes assets to withdraw, and calls _baseWithdraw. Reverts if redemption exceeds maxRedeem or if assets are zero.", "events": [ "Withdraw", "YieldAccrued" ], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "_maxAssetsSuppliableToAave", "signature": "_maxAssetsSuppliableToAave()", "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\n if (\n (reserveConfigMap & ~AAVE_ACTIVE_MASK == 0) ||\n (reserveConfigMap & ~AAVE_FROZEN_MASK != 0) ||\n (reserveConfigMap & ~AAVE_PAUSED_MASK != 0)\n ) {\n return 0;\n } else if (supplyCap == 0) {\n return type(uint256).max;\n } else {\n uint256 currentSupply = WadRayMath.rayMul(\n (ATOKEN.scaledTotalSupply() + uint256(reserveData.accruedToTreasury)),\n reserveData.liquidityIndex\n );\n uint256 supplyCapWithDecimals = supplyCap * 10 ** decimals();\n return supplyCapWithDecimals > currentSupply ? supplyCapWithDecimals - currentSupply : 0;\n }\n}", "comment": "Calculates the maximum amount of underlying assets that can be supplied to Aave.", "visibility": "internal", "modifiers": [], "parameters": [], "returns": "uint256 - Maximum amount.", "output_property": "Returns the maximum amount of underlying assets that can be deposited, based on Aave's reserve configuration and supply cap.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "_maxAssetsWithdrawableFromAave", "signature": "_maxAssetsWithdrawableFromAave()", "code": "function _maxAssetsWithdrawableFromAave() internal view returns (uint256) {\n AaveDataTypes.ReserveData memory reserveData = AAVE_POOL.getReserveData(address(UNDERLYING));\n\n uint256 reserveConfigMap = reserveData.configuration.data;\n\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 the maximum amount of underlying assets that can be withdrawn from Aave.", "visibility": "internal", "modifiers": [], "parameters": [], "returns": "uint256 - Maximum amount.", "output_property": "Returns the available liquidity of the underlying asset in Aave, or 0 if the reserve is inactive or paused.", "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)", "code": "function _baseDeposit(uint256 assets, uint256 shares, address depositor, address receiver, bool asAToken) private {\n // Need to transfer before minting or ERC777s could reenter.\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(ATOKEN.balanceOf(address(this)));\n\n _mint(receiver, shares);\n\n emit Deposit(depositor, receiver, assets, shares);\n}", "comment": "Handles the core deposit logic: transfers assets, updates state, mints shares.", "visibility": "private", "modifiers": [], "parameters": [ { "name": "assets", "type": "uint256", "description": "Amount of assets to deposit." }, { "name": "shares", "type": "uint256", "description": "Amount of shares to mint." }, { "name": "depositor", "type": "address", "description": "Address of the depositor." }, { "name": "receiver", "type": "address", "description": "Address to receive shares." }, { "name": "asAToken", "type": "bool", "description": "If true, transfer aTokens; otherwise, transfer underlying and supply to Aave." } ], "returns": "", "output_property": "Transfers assets, updates lastVaultBalance, mints shares, and emits Deposit event.", "events": [ "Deposit" ], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "_baseWithdraw", "signature": "_baseWithdraw(uint256 assets, uint256 shares, address owner, address receiver, address allowanceTarget, bool asAToken)", "code": "function _baseWithdraw(\n uint256 assets,\n uint256 shares,\n address owner,\n address receiver,\n address allowanceTarget,\n bool asAToken\n) private {\n if (allowanceTarget != owner) {\n _spendAllowance(owner, allowanceTarget, shares);\n }\n\n _burn(owner, shares);\n\n // Withdraw assets from Aave v3 and send to receiver\n if (asAToken) {\n ATOKEN.transfer(receiver, assets);\n } else {\n AAVE_POOL.withdraw(address(UNDERLYING), assets, receiver);\n }\n _s.lastVaultBalance = uint128(ATOKEN.balanceOf(address(this)));\n\n emit Withdraw(allowanceTarget, receiver, owner, assets, shares);\n}", "comment": "Handles the core withdrawal logic: burns shares, withdraws assets, updates state.", "visibility": "private", "modifiers": [], "parameters": [ { "name": "assets", "type": "uint256", "description": "Amount of assets to withdraw." }, { "name": "shares", "type": "uint256", "description": "Amount of shares to burn." }, { "name": "owner", "type": "address", "description": "Address of the share owner." }, { "name": "receiver", "type": "address", "description": "Address to receive the assets." }, { "name": "allowanceTarget", "type": "address", "description": "Address authorized to spend the shares." }, { "name": "asAToken", "type": "bool", "description": "If true, withdraw aTokens; otherwise, withdraw underlying from Aave." } ], "returns": "", "output_property": "Burns shares, withdraws assets from Aave (or transfers aTokens), updates lastVaultBalance, and emits Withdraw event.", "events": [ "Withdraw" ], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null } ], "structs": [ { "name": "VaultState", "definition": "struct VaultState {\n uint128 lastVaultBalance;\n uint128 accumulatedFees;\n uint64 fee;\n}", "description": "Holds the vault's internal state: the last known aToken balance, accumulated fees, and the current fee percentage." }, { "name": "EIP712Signature", "definition": "struct EIP712Signature {\n uint8 v;\n bytes32 r;\n bytes32 s;\n uint256 deadline;\n}", "description": "Holds the components of an EIP-712 signature and its deadline." } ], "modifiers": [ { "name": "onlyOwner", "definition": "Inherited from OpenZeppelin's OwnableUpgradeable", "purpose": "Restricts function access to the contract owner only." }, { "name": "initializer", "definition": "Inherited from OpenZeppelin's Initializable", "purpose": "Ensures a function is called only once during contract initialization." } ], "inheritance": [ "ERC4626Upgradeable", "OwnableUpgradeable", "EIP712Upgradeable", "ATokenVaultStorage" ], "call_graph": { "constructor": [ "IPoolAddressesProvider.getPool()", "IPool.getReserveData()" ], "initialize": [ "_transferOwnership()", "__ERC4626_init()", "__ERC20_init()", "__EIP712_init()", "_setFee()", "UNDERLYING.safeApprove()", "_handleDeposit()" ], "deposit": [ "_handleDeposit()" ], "depositATokens": [ "_handleDeposit()" ], "depositWithSig": [ "MetaTxHelpers._validateRecoveredAddress()", "MetaTxHelpers._calculateDigest()", "_domainSeparatorV4()", "_handleDeposit()" ], "depositATokensWithSig": [ "MetaTxHelpers._validateRecoveredAddress()", "MetaTxHelpers._calculateDigest()", "_domainSeparatorV4()", "_handleDeposit()" ], "mint": [ "_handleMint()" ], "mintWithATokens": [ "_handleMint()" ], "mintWithSig": [ "MetaTxHelpers._validateRecoveredAddress()", "MetaTxHelpers._calculateDigest()", "_domainSeparatorV4()", "_handleMint()" ], "mintWithATokensWithSig": [ "MetaTxHelpers._validateRecoveredAddress()", "MetaTxHelpers._calculateDigest()", "_domainSeparatorV4()", "_handleMint()" ], "withdraw": [ "_handleWithdraw()" ], "withdrawATokens": [ "_handleWithdraw()" ], "withdrawWithSig": [ "MetaTxHelpers._validateRecoveredAddress()", "MetaTxHelpers._calculateDigest()", "_domainSeparatorV4()", "_handleWithdraw()" ], "withdrawATokensWithSig": [ "MetaTxHelpers._validateRecoveredAddress()", "MetaTxHelpers._calculateDigest()", "_domainSeparatorV4()", "_handleWithdraw()" ], "redeem": [ "_handleRedeem()" ], "redeemAsATokens": [ "_handleRedeem()" ], "redeemWithSig": [ "MetaTxHelpers._validateRecoveredAddress()", "MetaTxHelpers._calculateDigest()", "_domainSeparatorV4()", "_handleRedeem()" ], "redeemWithATokensWithSig": [ "MetaTxHelpers._validateRecoveredAddress()", "MetaTxHelpers._calculateDigest()", "_domainSeparatorV4()", "_handleRedeem()" ], "maxDeposit": [ "_maxAssetsSuppliableToAave()" ], "maxMint": [ "_convertToShares()", "_maxAssetsSuppliableToAave()" ], "maxWithdraw": [ "_maxAssetsWithdrawableFromAave()", "_convertToAssets()", "balanceOf()" ], "maxRedeem": [ "_maxAssetsWithdrawableFromAave()", "_convertToShares()", "balanceOf()" ], "previewDeposit": [ "_convertToShares()", "_maxAssetsSuppliableToAave()" ], "previewMint": [ "_convertToAssets()", "_maxAssetsSuppliableToAave()" ], "previewWithdraw": [ "_maxAssetsWithdrawableFromAave()", "_convertToShares()" ], "previewRedeem": [ "_maxAssetsWithdrawableFromAave()", "_convertToAssets()" ], "domainSeparator": [ "_domainSeparatorV4()" ], "setFee": [ "_accrueYield()", "_setFee()" ], "withdrawFees": [ "_accrueYield()", "ATOKEN.transfer()", "ATOKEN.balanceOf()" ], "claimRewards": [ "IncentivizedERC20.getIncentivesController()", "IRewardsController.claimAllRewards()" ], "emergencyRescue": [ "IERC20Upgradeable.safeTransfer()" ], "totalAssets": [ "ATOKEN.balanceOf()", "getClaimableFees()" ], "getClaimableFees": [ "ATOKEN.balanceOf()" ], "getSigNonce": [], "getLastVaultBalance": [], "getFee": [], "_setFee": [], "_accrueYield": [ "ATOKEN.balanceOf()" ], "_handleDeposit": [ "maxDeposit()", "_accrueYield()", "super.previewDeposit()", "_convertToAssets()", "_baseDeposit()" ], "_handleMint": [ "maxMint()", "_accrueYield()", "super.previewMint()", "_baseDeposit()" ], "_handleWithdraw": [ "_accrueYield()", "maxWithdraw()", "super.previewWithdraw()", "_baseWithdraw()" ], "_handleRedeem": [ "_accrueYield()", "maxRedeem()", "super.previewRedeem()", "_baseWithdraw()" ], "_maxAssetsSuppliableToAave": [ "AAVE_POOL.getReserveData()", "WadRayMath.rayMul()", "ATOKEN.scaledTotalSupply()", "decimals()" ], "_maxAssetsWithdrawableFromAave": [ "AAVE_POOL.getReserveData()", "UNDERLYING.balanceOf()" ], "_baseDeposit": [ "ATOKEN.transferFrom()", "UNDERLYING.safeTransferFrom()", "AAVE_POOL.supply()", "ATOKEN.balanceOf()", "_mint()" ], "_baseWithdraw": [ "_spendAllowance()", "_burn()", "ATOKEN.transfer()", "AAVE_POOL.withdraw()", "ATOKEN.balanceOf()" ] }, "audit_issues": [ { "function": "deposit", "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 following deposits.", "status": "Fixed", "mitigation": "Fixed in PR#82 merged in commit 385b397.", "property": "When a user deposits an amount of underlying tokens, the vault's internal state variable lastVaultBalance should be updated to match the actual aToken balance. Due to rounding in aToken's balance updates, the lastVaultBalance could become mismatched, leading to a permanent revert state for any function that calls accrueYield().", "property_specification": "precondition: The vault has a current aToken balance B and lastVaultBalance = B. operation: A user deposits an amount of underlying tokens that, after conversion to aTokens, results in a balance B' that is not exactly represented due to rayMath rounding. postcondition: lastVaultBalance is updated to B'. actual vulnerability: The deposit function updates lastVaultBalance with the exact deposit amount, while aToken uses rayMath for its balance, causing a mismatch. This mismatch can accumulate over multiple operations, leading to a state where the lastVaultBalance is less than the actual balance, causing the yield calculation to revert." }, { "function": "getClaimableFees", "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 the vault has already charged fees. In every call to accrueYield(), the vault charges fees only from the new yield accrued since the last fee charge - ATOKEN.balanceOf(Vault) - _s.lastVaultBalance. Thus, it is expected that after every accrual, _s.lastVaultBalance will be equal to ATOKEN.balanceOf(Vault). 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 being updated with the exact assets amount passed to the function, aToken uses rayMath to update the ATOKEN.balanceOf(Vault). While the former is exact, the latter is subject to rounding and may differ from the passed assets amount.", "status": "Fixed", "mitigation": "Fixed in PR#86 merged in commit 385b397.", "property": "After every fee accrual, the vault's internal lastVaultBalance should equal the actual aToken balance. Due to rounding differences between how the vault updates lastVaultBalance (exact) and how aToken updates its balance (rayMath), a mismatch of up to 1 unit can occur per deposit/withdraw. Over many operations, this mismatch can accumulate, causing the vault to either undercharge fees (losing fee revenue) or overcharge fees (taking more than its fair share).", "property_specification": "precondition: The vault has lastVaultBalance = aToken balance (B) and accumulatedFees = F. operation: A user deposits or withdraws an amount that, after aToken's rayMath rounding, results in a new aToken balance B' that differs from the vault's update by Δ (where |Δ| ≤ 1). postcondition: lastVaultBalance = B' and fee accrual is based on the correct new yield. actual vulnerability: The vault updates lastVaultBalance with the exact asset amount, but the aToken balance uses rounded values. This creates a persistent difference that affects future fee calculations, leading to either loss of fees (if lastVaultBalance > actual balance) or overcharging fees (if lastVaultBalance < actual balance)." }, { "function": "previewRedeem", "issue": "Misinformation - previewRedeem() returns a larger amount of assets than an immediate redeem()", "severity": "Medium", "description": "Before calling previewRedeem, _accrueYield() is called, which causes a reduction in totalAssets() by feePercentage * (ATOKEN.balanceOf(vault) - lastVaultBalance).", "status": "Fixed", "mitigation": "Fixed in PR#82 merged in commit 385b397.", "property": "The previewRedeem function should return the amount of assets that would be received if the redemption were executed immediately. However, because the preview function does not account for the yield accrual that would happen during the actual redemption, it can return a larger amount than what is actually redeemable.", "property_specification": "precondition: The vault has some yield that has not been accrued, and a fee is set. operation: A user calls previewRedeem(shares) to see how many assets they would get for redeeming a certain number of shares. postcondition: The function returns the exact amount of assets that would be received if redeem(shares) were called immediately. actual vulnerability: previewRedeem does not simulate the fee deduction that would occur in _accrueYield() during the actual redeem transaction, leading to an overestimation of the assets that would be received." }, { "function": "withdrawFees", "issue": "Frontrun - avoiding fee charges for gifts given to the protocol", "severity": "Medium", "description": "A user can frontrun the fee withdrawal to avoid fees on newly gifted assets by triggering an accrual before the gift is made, then the gift is added, and then fees are withdrawn based on the stored accumulatedFees rather than recalculating.", "status": "Fixed", "mitigation": "Fixed in PR#82 merged in commit 385b397.", "property": "When the owner withdraws fees, they should receive fees on all yield earned up to that point, including any recent gifts to the protocol. However, an attacker can frontrun the fee withdrawal to make the gift after the accrual but before the withdrawal, causing the gift to be excluded from fees.", "property_specification": "precondition: The owner intends to withdraw fees. operation: An attacker triggers _accrueYield() (by making a small deposit) before the owner calls withdrawFees. After the accrual, the attacker gifts aTokens directly to the vault. postcondition: When the owner calls withdrawFees, the fees on the gifted amount are included. actual vulnerability: withdrawFees uses the stored accumulatedFees from before the gift, rather than recalculating fees on the new balance, allowing the gifted amount to be withdrawn by the owner without paying fees to the fee collector." }, { "function": "previewDeposit", "issue": "Non-compliance of the preview methods with the EIP4626 standard", "severity": "Informational", "description": "As per EIP4626, all the preview functions must not take into account any limitation of the system, like those returned by the max() methods. In the contract, the preview methods do take into account system limitations.", "status": "Acknowledged", "mitigation": "As per EIP4626, all the preview functions may revert due to other conditions that would also cause primary functions to revert. Relying on Aave is acceptable, given that primary functions are impacted by its limitations (e.g. users cannot withdraw if there is no available liquidity in the Aave Pool).", "property": "The previewDeposit function should return the number of shares that would be minted for a given deposit amount, independent of any deposit limits. However, this implementation caps the deposit amount by the maximum deposit limit, which violates the EIP-4626 requirement.", "property_specification": "precondition: The vault has a maximum deposit limit L. operation: A user calls previewDeposit(x) for an amount x > L. postcondition: previewDeposit(x) returns the same number of shares as it would for x = L (since the function should not account for limits). actual vulnerability: previewDeposit(x) returns the number of shares for L (due to min(x, L)), which is different from what it would return for an amount x' where x' > L, implying the function accounts for the limit, violating the EIP." }, { "function": "previewMint", "issue": "Non-compliance of the preview methods with the EIP4626 standard", "severity": "Informational", "description": "As per EIP4626, all the preview functions must not take into account any limitation of the system, like those returned by the max() methods. In the contract, the preview methods do take into account system limitations.", "status": "Acknowledged", "mitigation": "As per EIP4626, all the preview functions may revert due to other conditions that would also cause primary functions to revert. Relying on Aave is acceptable, given that primary functions are impacted by its limitations (e.g. users cannot withdraw if there is no available liquidity in the Aave Pool).", "property": "The previewMint function should return the amount of assets needed to mint a given number of shares, independent of any minting limits. However, this implementation caps the result by the maximum deposit limit, which violates the EIP-4626 requirement.", "property_specification": "precondition: The vault has a maximum deposit limit L. operation: A user calls previewMint(y) for a number of shares y that would require more than L in assets. postcondition: previewMint(y) returns the amount of assets needed to mint y shares without accounting for limits. actual vulnerability: previewMint(y) returns the assets for y shares but capped at L, implying the function accounts for the deposit limit, violating the EIP." }, { "function": "previewWithdraw", "issue": "Non-compliance of the preview methods with the EIP4626 standard", "severity": "Informational", "description": "As per EIP4626, all the preview functions must not take into account any limitation of the system, like those returned by the max() methods. In the contract, the preview methods do take into account system limitations.", "status": "Acknowledged", "mitigation": "As per EIP4626, all the preview functions may revert due to other conditions that would also cause primary functions to revert. Relying on Aave is acceptable, given that primary functions are impacted by its limitations (e.g. users cannot withdraw if there is no available liquidity in the Aave Pool).", "property": "The previewWithdraw function should return the number of shares needed to withdraw a given amount of assets, independent of any withdrawal limits. However, this implementation caps the withdrawal amount by the available liquidity, which violates the EIP-4626 requirement.", "property_specification": "precondition: The vault has available liquidity L. operation: A user calls previewWithdraw(x) for an amount x > L. postcondition: previewWithdraw(x) returns the number of shares needed to withdraw x assets without accounting for limits. actual vulnerability: previewWithdraw(x) returns the number of shares for L (due to min(x, L)), implying the function accounts for the withdrawal limit, violating the EIP." }, { "function": "previewRedeem", "issue": "Non-compliance of the preview methods with the EIP4626 standard", "severity": "Informational", "description": "As per EIP4626, all the preview functions must not take into account any limitation of the system, like those returned by the max() methods. In the contract, the preview methods do take into account system limitations.", "status": "Acknowledged", "mitigation": "As per EIP4626, all the preview functions may revert due to other conditions that would also cause primary functions to revert. Relying on Aave is acceptable, given that primary functions are impacted by its limitations (e.g. users cannot withdraw if there is no available liquidity in the Aave Pool).", "property": "The previewRedeem function should return the amount of assets that would be received for redeeming a given number of shares, independent of any redemption limits. However, this implementation caps the result by the available liquidity, which violates the EIP-4626 requirement.", "property_specification": "precondition: The vault has available liquidity L. operation: A user calls previewRedeem(y) for a number of shares y that would require more than L in assets. postcondition: previewRedeem(y) returns the amount of assets that would be received for y shares without accounting for limits. actual vulnerability: previewRedeem(y) returns the assets for y shares but capped at L, implying the function accounts for the withdrawal limit, violating the EIP." }, { "function": "totalAssets", "issue": "Non-compliance with EIP4626 standard - non-reverting functions", "severity": "Informational", "description": "As per EIP4626, the functions totalAssets, maxDeposit, maxMint, maxWithdraw, and maxRedeem must not revert by any means. In the contract, however, these functions may revert due to over/underflows of arithmetical computations.", "status": "Acknowledged", "mitigation": "Although conforming to a standard is important and even essential to a degree, there is likely little to be gained from modifying and altering the core code's functionality to adapt to the minutiae of the standard. As the vault relies inherently on the Aave Protocol, it is acceptable to revert due to Aave-specific calculations.", "property": "The totalAssets function should never revert. However, because it computes aToken balance minus claimable fees, it can revert if the claimable fees exceed the aToken balance due to previous mismatches.", "property_specification": "precondition: The vault's state has claimable fees greater than its aToken balance. operation: A user calls totalAssets(). postcondition: The function returns a value without reverting. actual vulnerability: The function attempts to compute ATOKEN.balanceOf(address(this)) - getClaimableFees(), which underflows and reverts, violating the EIP." } ], "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": "FeesWithdrawn", "parameters": "address indexed to, uint256 amount, uint256 lastVaultBalance, uint256 accumulatedFees", "description": "Emitted when fees are withdrawn." }, { "name": "FeeUpdated", "parameters": "uint256 oldFee, uint256 newFee", "description": "Emitted when the fee percentage is updated." }, { "name": "RewardsClaimed", "parameters": "address indexed to, address[] rewardsList, uint256[] claimedAmounts", "description": "Emitted when rewards are claimed from Aave." }, { "name": "EmergencyRescue", "parameters": "address indexed token, address indexed to, uint256 amount", "description": "Emitted when an ERC20 token is rescued from the contract." }, { "name": "YieldAccrued", "parameters": "uint256 newYield, uint256 newFees, uint256 newBalance", "description": "Emitted when yield is accrued and fees are calculated." }, { "name": "Transfer", "parameters": "address indexed from, address indexed to, uint256 value", "description": "Inherited from ERC20. Emitted when vault shares are transferred." }, { "name": "Approval", "parameters": "address indexed owner, address indexed spender, uint256 value", "description": "Inherited from ERC20. Emitted when an allowance is set." } ] }, { "contract_name": "WithdrawalQueue", "file_name": "WithdrawalQueue.sol", "metadata": { "license": "GPL-3.0", "solidity_version": "0.8.9", "description": "A contract for handling stETH withdrawal request queue within the Lido protocol", "author": "Lido" }, "state_variables": [ { "name": "BUNKER_MODE_SINCE_TIMESTAMP_POSITION", "type": "bytes32", "visibility": "internal", "mutability": "constant", "description": "Storage position for bunker mode activation timestamp" }, { "name": "BUNKER_MODE_DISABLED_TIMESTAMP", "type": "uint256", "visibility": "public", "mutability": "constant", "description": "Special value when bunker mode is inactive (max uint256)" }, { "name": "PAUSE_ROLE", "type": "bytes32", "visibility": "public", "mutability": "constant", "description": "Role identifier for pausing the contract" }, { "name": "RESUME_ROLE", "type": "bytes32", "visibility": "public", "mutability": "constant", "description": "Role identifier for resuming the contract" }, { "name": "FINALIZE_ROLE", "type": "bytes32", "visibility": "public", "mutability": "constant", "description": "Role identifier for finalizing withdrawals" }, { "name": "ORACLE_ROLE", "type": "bytes32", "visibility": "public", "mutability": "constant", "description": "Role identifier for oracle reports" }, { "name": "MIN_STETH_WITHDRAWAL_AMOUNT", "type": "uint256", "visibility": "public", "mutability": "constant", "description": "Minimum stETH amount for a withdrawal request" }, { "name": "MAX_STETH_WITHDRAWAL_AMOUNT", "type": "uint256", "visibility": "public", "mutability": "constant", "description": "Maximum stETH amount for a single withdrawal request" }, { "name": "STETH", "type": "IStETH", "visibility": "public", "mutability": "immutable", "description": "Lido stETH token contract" }, { "name": "WSTETH", "type": "IWstETH", "visibility": "public", "mutability": "immutable", "description": "Lido wstETH token contract" } ], "functions": [ { "name": "constructor", "signature": "constructor(IWstETH _wstETH)", "code": "constructor(IWstETH _wstETH) {\n WSTETH = _wstETH;\n STETH = WSTETH.stETH();\n}", "comment": "Constructor sets immutable stETH and wstETH references", "visibility": "public", "modifiers": [], "parameters": [ { "name": "_wstETH", "type": "IWstETH", "description": "Address of WstETH contract" } ], "returns": "", "output_property": "Initializes immutable variables WSTETH and STETH. No return value.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "initialize", "signature": "initialize(address _admin) external", "code": "function initialize(address _admin) external {\n if (_admin == address(0)) revert AdminZeroAddress();\n _initialize(_admin);\n}", "comment": "Initializes the contract storage", "visibility": "external", "modifiers": [], "parameters": [ { "name": "_admin", "type": "address", "description": "Admin address that can change roles" } ], "returns": "", "output_property": "Calls _initialize after checking admin not zero. Reverts if admin is zero address.", "events": [ "InitializedV1" ], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "resume", "signature": "resume() external", "code": "function resume() external {\n _checkPaused();\n _checkRole(RESUME_ROLE, msg.sender);\n _resume();\n}", "comment": "Resumes withdrawal requests placement and finalization", "visibility": "external", "modifiers": [], "parameters": [], "returns": "", "output_property": "Checks contract is paused, checks caller has RESUME_ROLE, then calls _resume(). Reverts if not paused or caller lacks role.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "pauseFor", "signature": "pauseFor(uint256 _duration) external onlyRole(PAUSE_ROLE)", "code": "function pauseFor(uint256 _duration) external onlyRole(PAUSE_ROLE) {\n _pauseFor(_duration);\n}", "comment": "Pause withdrawal requests for a given duration", "visibility": "external", "modifiers": [ "onlyRole(PAUSE_ROLE)" ], "parameters": [ { "name": "_duration", "type": "uint256", "description": "Pause duration in seconds" } ], "returns": "", "output_property": "Calls _pauseFor with duration. Reverts if caller lacks PAUSE_ROLE or if duration is zero.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "pauseUntil", "signature": "pauseUntil(uint256 _pauseUntilInclusive) external onlyRole(PAUSE_ROLE)", "code": "function pauseUntil(uint256 _pauseUntilInclusive) external onlyRole(PAUSE_ROLE) {\n _pauseUntil(_pauseUntilInclusive);\n}", "comment": "Pause withdrawal requests until a specific timestamp", "visibility": "external", "modifiers": [ "onlyRole(PAUSE_ROLE)" ], "parameters": [ { "name": "_pauseUntilInclusive", "type": "uint256", "description": "Last second to pause until (inclusive)" } ], "returns": "", "output_property": "Calls _pauseUntil with timestamp. Reverts if caller lacks PAUSE_ROLE or timestamp is in the past.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "requestWithdrawals", "signature": "requestWithdrawals(uint256[] calldata amounts, address _owner) public returns (uint256[] memory requestIds)", "code": "function requestWithdrawals(uint256[] calldata amounts, address _owner) public returns (uint256[] memory requestIds) {\n _checkResumed();\n if (_owner == address(0)) _owner = msg.sender;\n requestIds = new uint256[](amounts.length);\n for (uint256 i = 0; i < amounts.length; ++i) {\n _checkWithdrawalRequestAmount(amounts[i]);\n requestIds[i] = _requestWithdrawal(amounts[i], _owner);\n }\n}", "comment": "Creates withdrawal requests for stETH amounts", "visibility": "public", "modifiers": [], "parameters": [ { "name": "amounts", "type": "uint256[]", "description": "Array of stETH amounts" }, { "name": "_owner", "type": "address", "description": "Owner of requests (zero uses msg.sender)" } ], "returns": "uint256[] - Array of created request IDs", "output_property": "Checks contract is resumed, validates each amount, transfers stETH from caller, mints request tokens. Reverts if paused, amount too small/large, or transfer fails.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "requestWithdrawalsWstETH", "signature": "requestWithdrawalsWstETH(uint256[] calldata amounts, address _owner) public returns (uint256[] memory requestIds)", "code": "function requestWithdrawalsWstETH(uint256[] calldata amounts, address _owner) public returns (uint256[] memory requestIds) {\n _checkResumed();\n if (_owner == address(0)) _owner = msg.sender;\n requestIds = new uint256[](amounts.length);\n for (uint256 i = 0; i < amounts.length; ++i) {\n requestIds[i] = _requestWithdrawalWstETH(amounts[i], _owner);\n }\n}", "comment": "Creates withdrawal requests for wstETH amounts", "visibility": "public", "modifiers": [], "parameters": [ { "name": "amounts", "type": "uint256[]", "description": "Array of wstETH amounts" }, { "name": "_owner", "type": "address", "description": "Owner of requests" } ], "returns": "uint256[] - Array of request IDs", "output_property": "Checks resumed, unwraps wstETH to stETH, validates amount, transfers wstETH from caller, creates request. Reverts if paused or transfer fails.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "requestWithdrawalsWithPermit", "signature": "requestWithdrawalsWithPermit(uint256[] calldata _amounts, address _owner, PermitInput calldata _permit) external returns (uint256[] memory requestIds)", "code": "function requestWithdrawalsWithPermit(uint256[] calldata _amounts, address _owner, PermitInput calldata _permit) external returns (uint256[] memory requestIds) {\n STETH.permit(msg.sender, address(this), _permit.value, _permit.deadline, _permit.v, _permit.r, _permit.s);\n return requestWithdrawals(_amounts, _owner);\n}", "comment": "Request withdrawals using stETH permit for approval", "visibility": "external", "modifiers": [], "parameters": [ { "name": "_amounts", "type": "uint256[]", "description": "Array of stETH amounts" }, { "name": "_owner", "type": "address", "description": "Request owner" }, { "name": "_permit", "type": "PermitInput", "description": "Permit signature data" } ], "returns": "uint256[] - Request IDs", "output_property": "Calls stETH.permit to set allowance, then calls requestWithdrawals. Reverts if permit invalid.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "requestWithdrawalsWstETHWithPermit", "signature": "requestWithdrawalsWstETHWithPermit(uint256[] calldata _amounts, address _owner, PermitInput calldata _permit) external returns (uint256[] memory requestIds)", "code": "function requestWithdrawalsWstETHWithPermit(uint256[] calldata _amounts, address _owner, PermitInput calldata _permit) external returns (uint256[] memory requestIds) {\n WSTETH.permit(msg.sender, address(this), _permit.value, _permit.deadline, _permit.v, _permit.r, _permit.s);\n return requestWithdrawalsWstETH(_amounts, _owner);\n}", "comment": "Request withdrawals using wstETH permit", "visibility": "external", "modifiers": [], "parameters": [ { "name": "_amounts", "type": "uint256[]", "description": "Array of wstETH amounts" }, { "name": "_owner", "type": "address", "description": "Request owner" }, { "name": "_permit", "type": "PermitInput", "description": "Permit signature" } ], "returns": "uint256[] - Request IDs", "output_property": "Calls wstETH.permit, then requestWithdrawalsWstETH.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "getWithdrawalRequests", "signature": "getWithdrawalRequests(address _owner) external view returns (uint256[] memory requestsIds)", "code": "function getWithdrawalRequests(address _owner) external view returns (uint256[] memory requestsIds) {\n return _getRequestsByOwner()[_owner].values();\n}", "comment": "Returns all withdrawal request IDs belonging to an owner", "visibility": "external", "modifiers": [], "parameters": [ { "name": "_owner", "type": "address", "description": "Owner address" } ], "returns": "uint256[] - Array of request IDs", "output_property": "View function returning the set of request IDs for the owner. May be expensive if many requests.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "getWithdrawalStatus", "signature": "getWithdrawalStatus(uint256[] calldata _requestIds) external view returns (WithdrawalRequestStatus[] memory statuses)", "code": "function getWithdrawalStatus(uint256[] calldata _requestIds) external view returns (WithdrawalRequestStatus[] memory statuses) {\n statuses = new WithdrawalRequestStatus[](_requestIds.length);\n for (uint256 i = 0; i < _requestIds.length; ++i) {\n statuses[i] = _getStatus(_requestIds[i]);\n }\n}", "comment": "Returns statuses for an array of request IDs", "visibility": "external", "modifiers": [], "parameters": [ { "name": "_requestIds", "type": "uint256[]", "description": "Array of request IDs" } ], "returns": "WithdrawalRequestStatus[] - Array of status structs", "output_property": "Iterates over request IDs and fetches each status. Reverts if any ID is invalid.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "getClaimableEther", "signature": "getClaimableEther(uint256[] calldata _requestIds, uint256[] calldata _hints) external view returns (uint256[] memory claimableEthValues)", "code": "function getClaimableEther(uint256[] calldata _requestIds, uint256[] calldata _hints) external view returns (uint256[] memory claimableEthValues) {\n claimableEthValues = new uint256[](_requestIds.length);\n for (uint256 i = 0; i < _requestIds.length; ++i) {\n claimableEthValues[i] = _getClaimableEther(_requestIds[i], _hints[i]);\n }\n}", "comment": "Returns claimable ETH amounts for withdrawal requests", "visibility": "external", "modifiers": [], "parameters": [ { "name": "_requestIds", "type": "uint256[]", "description": "Array of request IDs" }, { "name": "_hints", "type": "uint256[]", "description": "Checkpoint hints for each ID" } ], "returns": "uint256[] - Array of claimable ETH amounts", "output_property": "Calls _getClaimableEther for each request. Reverts if hints are invalid or request IDs out of range.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "claimWithdrawalsTo", "signature": "claimWithdrawalsTo(uint256[] calldata _requestIds, uint256[] calldata _hints, address _recipient) external", "code": "function claimWithdrawalsTo(uint256[] calldata _requestIds, uint256[] calldata _hints, address _recipient) external {\n if (_recipient == address(0)) revert ZeroRecipient();\n for (uint256 i = 0; i < _requestIds.length; ++i) {\n _claim(_requestIds[i], _hints[i], _recipient);\n _emitTransfer(msg.sender, address(0), _requestIds[i]);\n }\n}", "comment": "Claims finalized withdrawal requests and sends ETH to recipient", "visibility": "external", "modifiers": [], "parameters": [ { "name": "_requestIds", "type": "uint256[]", "description": "Request IDs to claim" }, { "name": "_hints", "type": "uint256[]", "description": "Checkpoint hints" }, { "name": "_recipient", "type": "address", "description": "Recipient of ETH" } ], "returns": "", "output_property": "Claims each request, transfers ETH to recipient, emits transfer event. Reverts if recipient zero, any request not owned by caller, not finalized, or already claimed.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "claimWithdrawals", "signature": "claimWithdrawals(uint256[] calldata _requestIds, uint256[] calldata _hints) external", "code": "function claimWithdrawals(uint256[] calldata _requestIds, uint256[] calldata _hints) external {\n for (uint256 i = 0; i < _requestIds.length; ++i) {\n _claim(_requestIds[i], _hints[i], msg.sender);\n _emitTransfer(msg.sender, address(0), _requestIds[i]);\n }\n}", "comment": "Claims finalized withdrawal requests to the caller", "visibility": "external", "modifiers": [], "parameters": [ { "name": "_requestIds", "type": "uint256[]", "description": "Request IDs to claim" }, { "name": "_hints", "type": "uint256[]", "description": "Checkpoint hints" } ], "returns": "", "output_property": "Claims each request, sends ETH to msg.sender. Same revert conditions as claimWithdrawalsTo.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "claimWithdrawal", "signature": "claimWithdrawal(uint256 _requestId) external", "code": "function claimWithdrawal(uint256 _requestId) external {\n _claim(_requestId, _findCheckpointHint(_requestId, 1, getLastCheckpointIndex()), msg.sender);\n _emitTransfer(msg.sender, address(0), _requestId);\n}", "comment": "Claims a single withdrawal request using linear search for hint", "visibility": "external", "modifiers": [], "parameters": [ { "name": "_requestId", "type": "uint256", "description": "Request ID to claim" } ], "returns": "", "output_property": "Finds checkpoint hint via linear search (may OOG for large queues), then claims. Reverts if request not finalized or not owned.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "findCheckpointHints", "signature": "findCheckpointHints(uint256[] calldata _requestIds, uint256 _firstIndex, uint256 _lastIndex) external view returns (uint256[] memory hintIds)", "code": "function findCheckpointHints(uint256[] calldata _requestIds, uint256 _firstIndex, uint256 _lastIndex) external view returns (uint256[] memory hintIds) {\n hintIds = new uint256[](_requestIds.length);\n uint256 prevRequestId = 0;\n for (uint256 i = 0; i < _requestIds.length; ++i) {\n if (_requestIds[i] < prevRequestId) revert RequestIdsNotSorted();\n hintIds[i] = _findCheckpointHint(_requestIds[i], _firstIndex, _lastIndex);\n _firstIndex = hintIds[i];\n prevRequestId = _requestIds[i];\n }\n}", "comment": "Finds checkpoint hints for a sorted array of request IDs", "visibility": "external", "modifiers": [], "parameters": [ { "name": "_requestIds", "type": "uint256[]", "description": "Sorted array of request IDs" }, { "name": "_firstIndex", "type": "uint256", "description": "Left boundary of search range" }, { "name": "_lastIndex", "type": "uint256", "description": "Right boundary" } ], "returns": "uint256[] - Array of hints", "output_property": "Validates sorting, finds hint for each ID. Reverts if IDs not sorted.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "finalize", "signature": "finalize(uint256[] calldata _batches, uint256 _maxShareRate) external payable", "code": "function finalize(uint256[] calldata _batches, uint256 _maxShareRate) external payable {\n _checkResumed();\n _checkRole(FINALIZE_ROLE, msg.sender);\n _finalize(_batches, msg.value, _maxShareRate);\n}", "comment": "Finalizes withdrawal requests from last finalized up to specified batches", "visibility": "external", "modifiers": [], "parameters": [ { "name": "_batches", "type": "uint256[]", "description": "Batch data for finalization" }, { "name": "_maxShareRate", "type": "uint256", "description": "Maximum allowed share rate" } ], "returns": "", "output_property": "Checks resumed, caller has FINALIZE_ROLE, then calls _finalize with sent ETH. Vulnerable to discount factor miscalculation (Cr-01), batch locking funds (H-05), and incomplete share rate recovery (M-04).", "events": [], "vulnerable": false, "vulnerability_details": { "issue": "Withdrawal Finalization Affected by other users in batch (DiscountFactor issue)", "severity": "Critical", "description": "If two users in the same batch entered with different share rates and a slash occurs, the finalized amount is weighted incorrectly, causing unjust ETH losses for some users. A user can exit before a slashing report to lose less at the expense of others.", "mitigation": "Fixed in commit 336b6f3. The withdrawals finalization process was changed to remove the discount factor in favor of share rate batch-wise calculation." }, "property": "Each withdrawal request should receive ETH proportional to its share rate at finalization, independent of other requests in the same batch.", "property_specification": { "precondition": "User A has request with shareRate RA, User B with shareRate RB, RA < RB, both in same batch. Slashing causes final shareRate S where RA < S < RB", "operation": "finalize(batch)", "postcondition": "A receives amount based on RA, B receives amount based on RB", "actual": "Weighted average calculation causes A to receive less than expected and B to receive more, with A subsidizing B's losses." } }, { "name": "onOracleReport", "signature": "onOracleReport(bool _isBunkerModeNow, uint256 _sinceTimestamp, uint256 _currentReportTimestamp) external", "code": "function onOracleReport(bool _isBunkerModeNow, uint256 _sinceTimestamp, uint256 _currentReportTimestamp) external {\n _checkRole(ORACLE_ROLE, msg.sender);\n if (_sinceTimestamp >= block.timestamp) revert InvalidReportTimestamp();\n if (_currentReportTimestamp >= block.timestamp) revert InvalidReportTimestamp();\n _setLastReportTimestamp(_currentReportTimestamp);\n bool isBunkerModeWasSetBefore = isBunkerModeActive();\n if (_isBunkerModeNow != isBunkerModeWasSetBefore) {\n if (_isBunkerModeNow) {\n BUNKER_MODE_SINCE_TIMESTAMP_POSITION.setStorageUint256(_sinceTimestamp);\n emit BunkerModeEnabled(_sinceTimestamp);\n } else {\n BUNKER_MODE_SINCE_TIMESTAMP_POSITION.setStorageUint256(BUNKER_MODE_DISABLED_TIMESTAMP);\n emit BunkerModeDisabled();\n }\n }\n}", "comment": "Updates bunker mode state and last report timestamp, callable by oracle", "visibility": "external", "modifiers": [], "parameters": [ { "name": "_isBunkerModeNow", "type": "bool", "description": "Whether bunker mode is active" }, { "name": "_sinceTimestamp", "type": "uint256", "description": "Bunker mode start timestamp" }, { "name": "_currentReportTimestamp", "type": "uint256", "description": "Current report timestamp" } ], "returns": "", "output_property": "Validates timestamps not in future, sets last report timestamp, updates bunker mode if changed. Reverts if caller lacks ORACLE_ROLE or timestamps invalid.", "events": [ "BunkerModeEnabled", "BunkerModeDisabled" ], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "isBunkerModeActive", "signature": "isBunkerModeActive() public view returns (bool)", "code": "function isBunkerModeActive() public view returns (bool) {\n return bunkerModeSinceTimestamp() < BUNKER_MODE_DISABLED_TIMESTAMP;\n}", "comment": "Checks if bunker mode is active", "visibility": "public", "modifiers": [], "parameters": [], "returns": "bool - True if bunker mode active", "output_property": "View function comparing stored timestamp with disabled sentinel.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "bunkerModeSinceTimestamp", "signature": "bunkerModeSinceTimestamp() public view returns (uint256)", "code": "function bunkerModeSinceTimestamp() public view returns (uint256) {\n return BUNKER_MODE_SINCE_TIMESTAMP_POSITION.getStorageUint256();\n}", "comment": "Returns bunker mode activation timestamp", "visibility": "public", "modifiers": [], "parameters": [], "returns": "uint256 - Timestamp or BUNKER_MODE_DISABLED_TIMESTAMP", "output_property": "View function reading from storage position.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "_emitTransfer", "signature": "_emitTransfer(address from, address to, uint256 _requestId) internal virtual", "code": "function _emitTransfer(address from, address to, uint256 _requestId) internal virtual;", "comment": "Virtual function to emit ERC721 Transfer event", "visibility": "internal", "modifiers": [], "parameters": [ { "name": "from", "type": "address", "description": "Sender" }, { "name": "to", "type": "address", "description": "Recipient" }, { "name": "_requestId", "type": "uint256", "description": "Token ID" } ], "returns": "", "output_property": "Implemented in inheriting contract to emit ERC721 Transfer. No state changes.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "_initialize", "signature": "_initialize(address _admin) internal", "code": "function _initialize(address _admin) internal {\n _initializeQueue();\n _pauseFor(PAUSE_INFINITELY);\n _initializeContractVersionTo(1);\n _grantRole(DEFAULT_ADMIN_ROLE, _admin);\n BUNKER_MODE_SINCE_TIMESTAMP_POSITION.setStorageUint256(BUNKER_MODE_DISABLED_TIMESTAMP);\n emit InitializedV1(_admin);\n}", "comment": "Internal initialization helper", "visibility": "internal", "modifiers": [], "parameters": [ { "name": "_admin", "type": "address", "description": "Admin address" } ], "returns": "", "output_property": "Initializes queue, pauses contract infinitely, sets version to 1, grants admin role, disables bunker mode, emits event. No revert conditions beyond admin zero check in caller.", "events": [ "InitializedV1" ], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "_requestWithdrawal", "signature": "_requestWithdrawal(uint256 _amountOfStETH, address _owner) internal returns (uint256 requestId)", "code": "function _requestWithdrawal(uint256 _amountOfStETH, address _owner) internal returns (uint256 requestId) {\n STETH.transferFrom(msg.sender, address(this), _amountOfStETH);\n uint256 amountOfShares = STETH.getSharesByPooledEth(_amountOfStETH);\n requestId = _enqueue(uint128(_amountOfStETH), uint128(amountOfShares), _owner);\n _emitTransfer(address(0), _owner, requestId);\n}", "comment": "Internal function to create stETH withdrawal request", "visibility": "internal", "modifiers": [], "parameters": [ { "name": "_amountOfStETH", "type": "uint256", "description": "Amount of stETH" }, { "name": "_owner", "type": "address", "description": "Request owner" } ], "returns": "uint256 - New request ID", "output_property": "Transfers stETH, calculates shares, enqueues request, emits transfer. Reverts if transfer fails or amount invalid.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "_requestWithdrawalWstETH", "signature": "_requestWithdrawalWstETH(uint256 _amountOfWstETH, address _owner) internal returns (uint256 requestId)", "code": "function _requestWithdrawalWstETH(uint256 _amountOfWstETH, address _owner) internal returns (uint256 requestId) {\n WSTETH.transferFrom(msg.sender, address(this), _amountOfWstETH);\n uint256 amountOfStETH = WSTETH.unwrap(_amountOfWstETH);\n _checkWithdrawalRequestAmount(amountOfStETH);\n uint256 amountOfShares = STETH.getSharesByPooledEth(amountOfStETH);\n requestId = _enqueue(uint128(amountOfStETH), uint128(amountOfShares), _owner);\n _emitTransfer(address(0), _owner, requestId);\n}", "comment": "Internal function to create wstETH withdrawal request", "visibility": "internal", "modifiers": [], "parameters": [ { "name": "_amountOfWstETH", "type": "uint256", "description": "Amount of wstETH" }, { "name": "_owner", "type": "address", "description": "Request owner" } ], "returns": "uint256 - New request ID", "output_property": "Transfers wstETH, unwraps to stETH, validates amount, calculates shares, enqueues, emits transfer.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "_checkWithdrawalRequestAmount", "signature": "_checkWithdrawalRequestAmount(uint256 _amountOfStETH) internal pure", "code": "function _checkWithdrawalRequestAmount(uint256 _amountOfStETH) internal pure {\n if (_amountOfStETH < MIN_STETH_WITHDRAWAL_AMOUNT) {\n revert RequestAmountTooSmall(_amountOfStETH);\n }\n if (_amountOfStETH > MAX_STETH_WITHDRAWAL_AMOUNT) {\n revert RequestAmountTooLarge(_amountOfStETH);\n }\n}", "comment": "Validates withdrawal request amount against min and max", "visibility": "internal", "modifiers": [], "parameters": [ { "name": "_amountOfStETH", "type": "uint256", "description": "Amount to validate" } ], "returns": "", "output_property": "Pure function that reverts if amount < 100 or > 1000 ETH.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null }, { "name": "_getClaimableEther", "signature": "_getClaimableEther(uint256 _requestId, uint256 _hint) internal view returns (uint256)", "code": "function _getClaimableEther(uint256 _requestId, uint256 _hint) internal view returns (uint256) {\n if (_requestId == 0 || _requestId > getLastRequestId()) revert InvalidRequestId(_requestId);\n if (_requestId > getLastFinalizedRequestId()) return 0;\n WithdrawalRequest storage request = _getQueue()[_requestId];\n if (request.claimed) return 0;\n return _calculateClaimableEther(request, _requestId, _hint);\n}", "comment": "Returns claimable ether for a request using hint", "visibility": "internal", "modifiers": [], "parameters": [ { "name": "_requestId", "type": "uint256", "description": "Request ID" }, { "name": "_hint", "type": "uint256", "description": "Checkpoint hint" } ], "returns": "uint256 - Claimable ETH amount", "output_property": "Checks request validity, finalization, claimed status, then calculates claimable amount. Returns 0 if not finalized or already claimed.", "events": [], "vulnerable": false, "vulnerability_details": null, "property": null, "property_specification": null } ], "structs": [ { "name": "PermitInput", "definition": "struct PermitInput { uint256 value; uint256 deadline; uint8 v; bytes32 r; bytes32 s; }", "description": "EIP-2612 permit signature parameters" } ], "modifiers": [ { "name": "onlyRole", "definition": "Inherited from AccessControl: require(hasRole(role, msg.sender), 'AccessControl:...')", "purpose": "Restricts function access to accounts with a specific role" } ], "inheritance": [ "AccessControlEnumerable", "PausableUntil", "WithdrawalQueueBase", "Versioned" ], "call_graph": { "constructor": [ "WSTETH.stETH()" ], "initialize": [ "_initialize" ], "resume": [ "_checkPaused", "_checkRole", "_resume" ], "pauseFor": [ "_pauseFor" ], "pauseUntil": [ "_pauseUntil" ], "requestWithdrawals": [ "_checkResumed", "_checkWithdrawalRequestAmount", "_requestWithdrawal" ], "requestWithdrawalsWstETH": [ "_checkResumed", "_requestWithdrawalWstETH" ], "requestWithdrawalsWithPermit": [ "STETH.permit", "requestWithdrawals" ], "requestWithdrawalsWstETHWithPermit": [ "WSTETH.permit", "requestWithdrawalsWstETH" ], "getWithdrawalRequests": [ "_getRequestsByOwner" ], "getWithdrawalStatus": [ "_getStatus" ], "getClaimableEther": [ "_getClaimableEther" ], "claimWithdrawalsTo": [ "_claim", "_emitTransfer" ], "claimWithdrawals": [ "_claim", "_emitTransfer" ], "claimWithdrawal": [ "_findCheckpointHint", "_claim", "_emitTransfer" ], "findCheckpointHints": [ "_findCheckpointHint" ], "finalize": [ "_checkResumed", "_checkRole", "_finalize" ], "onOracleReport": [ "_checkRole", "_setLastReportTimestamp", "isBunkerModeActive", "bunkerModeSinceTimestamp" ], "isBunkerModeActive": [ "bunkerModeSinceTimestamp" ], "bunkerModeSinceTimestamp": [], "_emitTransfer": [], "_initialize": [ "_initializeQueue", "_pauseFor", "_initializeContractVersionTo", "_grantRole" ], "_requestWithdrawal": [ "STETH.transferFrom", "STETH.getSharesByPooledEth", "_enqueue", "_emitTransfer" ], "_requestWithdrawalWstETH": [ "WSTETH.transferFrom", "WSTETH.unwrap", "_checkWithdrawalRequestAmount", "STETH.getSharesByPooledEth", "_enqueue", "_emitTransfer" ], "_checkWithdrawalRequestAmount": [], "_getClaimableEther": [ "getLastRequestId", "getLastFinalizedRequestId", "_getQueue", "_calculateClaimableEther" ] }, "audit_issues": [ { "function": "finalize", "issue": "Withdrawal Finalization Affected by other users in batch (DiscountFactor issue)", "severity": "Critical", "description": "If two users in the same batch entered with different share rates and a slash occurs, the finalized amount is weighted incorrectly, causing unjust ETH losses for some users. A user can exit before a slashing report to lose less at the expense of others.", "status": "Fixed", "mitigation": "Fixed in commit 336b6f3. The withdrawals finalization process was changed to remove the discount factor in favor of share rate batch-wise calculation approach.", "property": "Each withdrawal request should receive ETH proportional to its share rate at finalization, independent of other requests in the same batch.", "property_specification": "precondition: User A has request with shareRate RA, User B with shareRate RB, RA < RB, both in same batch. Slashing causes final shareRate S where RA < S < RB. operation: finalize(batch). postcondition: A receives amount based on RA, B receives amount based on RB. actual vulnerability: Weighted average calculation causes A to receive less than expected and B to receive more, with A subsidizing B's losses." }, { "function": "finalize", "issue": "Wrong batching in the oracle report could permanently lock funds in the withdrawalQueue contract", "severity": "High", "description": "If oracles put requests in the same batch that should not be batched together, the finalization may send more ETH than needed, locking excess funds permanently in the withdrawalQueue contract.", "status": "Acknowledged", "mitigation": "The contract is upgradeable; locked funds can be recovered via DAO vote by upgrading the implementation. Also strict on-chain batch validation was considered too complex.", "property": "The total ETH sent to the withdrawalQueue for finalization must exactly match the sum of claimable amounts for all requests in the batch.", "property_specification": "precondition: Two requests with different share rates are placed in the same batch. operation: finalize(batch) with ETH amount calculated based on incorrect batching. postcondition: All claimable amounts sum to the ETH sent. actual vulnerability: More ETH is sent than can be claimed, leaving residual ETH permanently locked in the contract." }, { "function": "finalize", "issue": "Finalization shareRate recovery wouldn't fully recover", "severity": "Medium", "description": "When a recovery occurs due to users in the queue with lower rates, the recovering user gets a slightly lower rate than if calculated in separate batches. This contradicts documentation that users fully recover with the protocol.", "status": "Acknowledged", "mitigation": "Accepted as intended behavior; edge cases are rare and deviations are tiny in real scenarios. Complex iterative approach would introduce more risks.", "property": "Users who entered the queue with higher share rates should fully recover when the protocol's share rate recovers, regardless of batching.", "property_specification": "precondition: User A (shareRate 1.0) and User B (shareRate 2.0) are in same batch. Final shareRate after recovery is 1.5. operation: finalize(batch). postcondition: User B receives 1.666 ETH (as if finalized separately). actual vulnerability: User B receives only 1.5 ETH, losing value compared to separate batch finalization." } ], "events": [ { "name": "InitializedV1", "parameters": "address _admin", "description": "Emitted when the contract is initialized" }, { "name": "BunkerModeEnabled", "parameters": "uint256 _sinceTimestamp", "description": "Emitted when bunker mode is enabled" }, { "name": "BunkerModeDisabled", "parameters": "", "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": { "precondition": "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.", "postcondition": "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": "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": "precondition: 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. postcondition: 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_OLD", "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": { "precondition": "User has debt balance D, total supply S", "operation": "mint(user, amount, rate) where amount > 0 but amount.wadToRay() == 0 due to rounding.", "postcondition": "User's debt balance increases by amount (scaled appropriately)", "actual": "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": "precondition: User has debt balance D, total supply S. operation: mint(user, amount, rate) where amount > 0 but amount.wadToRay() == 0 due to rounding. postcondition: 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_OLD", "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": { "precondition": "Owner address can be any address.", "operation": "initialize(owner, ...) where owner == address(0).", "postcondition": "The contract owner is address(0) and administrative functions are permanently locked.", "actual": "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": { "precondition": "Vault state is consistent, lastVaultBalance equals ATOKEN.balanceOf(vault).", "operation": "deposit(assets) where assets is not a multiple of liquidity index.", "postcondition": "Vault remains consistent, lastVaultBalance == ATOKEN.balanceOf(vault) after deposit.", "actual": "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": { "precondition": "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.", "postcondition": "Deposit should succeed because no new underlying is supplied to Aave.", "actual": "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": { "precondition": "lastVaultBalance = ATOKEN.balanceOf(vault) = B.", "operation": "withdraw(assets) where assets is not a multiple of the liquidity index.", "postcondition": "lastVaultBalance' = ATOKEN.balanceOf(vault)' = B - assets_actual.", "actual": "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": { "precondition": "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.", "postcondition": "The gift amount is included in new yield and fees are charged on it.", "actual": "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": "precondition: lastVaultBalance == ATOKEN.balanceOf(vault). operation: deposit(assets) where assets is not a multiple of the liquidity index. postcondition: 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": "precondition: Aave pool has a supply cap and the remaining cap is less than the underlying value of the aTokens being deposited. operation: depositATokens(assets). postcondition: 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": "precondition: lastVaultBalance = ATOKEN.balanceOf(vault). operation: deposit(assets) or withdraw(assets) where assets is not a multiple of the liquidity index. postcondition: 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": "precondition: None. operation: initialize(owner = address(0), ...). postcondition: 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": "precondition: 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. postcondition: 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" } ] } ]