diff --git a/defi-security/crosschain_bridges/erc777/README.md b/defi-security/crosschain_bridges/erc777/README.md new file mode 100644 index 00000000..3b122262 --- /dev/null +++ b/defi-security/crosschain_bridges/erc777/README.md @@ -0,0 +1,69 @@ +# ERC777 Bridge Vulnerability: Reentrancy Attack in Token Accounting + +Cross-chain bridges enable token transfers between different layers (e.g., L1 and L2). However, when these bridges support **ERC-777** tokens, their advanced callback hooks can introduce **reentrancy** vulnerabilities—allowing attackers to manipulate token accounting before the original transaction completes. + +--- + +## Fee-on-Transfer Tokens + +**Fee-on-transfer** tokens are tokens that automatically deduct a fee whenever they are transferred. As a result: + +- The **sender** specifies an `amount` to send. +- The **receiver** ends up with a lesser amount (the difference is taken as a fee). +- The bridging contract cannot simply trust the `amount` passed to the `transfer` function. + +Hence, bridges often rely on computing `balanceAfter - balanceBefore` to determine the **exact** amount that actually arrived in the contract. This practice accommodates fee-on-transfer tokens, but also opens up potential reentrancy risks when dealing with ERC-777 tokens if the contract is not safeguarded. + +--- + +## How the Vulnerability Arises + +1. **ERC-777 Callback Hooks** + - Unlike standard ERC-20 tokens, ERC-777 supports hooks via the ERC-1820 registry. + - Attackers can register a contract to be notified (`tokensToSend`) whenever a transfer occurs, creating an opening for reentrancy. + +2. **Balance-Based Bridging Logic** + - Bridges measure `balanceBefore` and `balanceAfter` to handle fee-on-transfer tokens. + - If a malicious contract re-enters during the transfer, the contract may incorrectly calculate how many tokens were deposited. + +3. **Lack of Reentrancy Protection** + - Without proper guards or a safe pattern, the bridge function can be invoked **twice** (or more) within a single flow, causing inaccurate state updates. + +--- +## Attack Scenario + +1. **Attacker Registers a Malicious Sender Contract** + - The attacker sets their contract as an `ERC777TokensSender` in the ERC-1820 registry. + +2. **Initial Bridge Call** + - The attacker calls `bridgeToken` with 500 tokens. + - The bridge records `balanceBefore = 0`, then initiates `safeTransferFrom`. + +3. **Reentrancy During Callback** + - `tokensToSend` is triggered in the attacker’s contract, which **re-enters** `bridgeToken` to transfer another 500 tokens. + - Since the first call hasn’t updated state yet, the second call still sees `balanceBefore = 0`. + +4. **Combined Transfers** + - By the time the original call finishes, `balanceAfter` is 1000 in the original call, but the logic credits 1500 tokens on L2 (500 from the reentrant call plus the erroneously calculated amount from the first call). + +--- + +## Example Code (Vulnerable) + +```solidity +function bridgeToken(address token, uint256 amount) external { + uint256 balanceBefore = IERC20(token).balanceOf(address(this)); + + // Transfer tokens from msg.sender to the bridge + IERC20(token).safeTransferFrom(msg.sender, address(this), amount); + + uint256 balanceAfter = IERC20(token).balanceOf(address(this)); + uint256 bridgedAmount = balanceAfter - balanceBefore; +} +``` + +## Mitigations + + + +Leverage mechanisms like ReentrancyGuard or similar logic that disallows nested calls to vulnerable functions. \ No newline at end of file diff --git a/defi-security/crosschain_bridges/self_calls/README.md b/defi-security/crosschain_bridges/self_calls/README.md new file mode 100644 index 00000000..d47c616b --- /dev/null +++ b/defi-security/crosschain_bridges/self_calls/README.md @@ -0,0 +1,29 @@ +# Avoiding Privilege Escalation via Self-Calls in Cross-Chain Bridges + +In many cross-chain bridge designs, certain contracts (often called “managers” or “governors”) hold **privileged roles**. These roles allow them to manage core operations like minting/burning tokens, updating trusted validators, or forwarding user transactions across chains. If these privileged contracts can be **invoked by user-controlled data**—especially in a way that allows **self-calls**—an attacker can craft malicious parameters that make the bridge invoke its own privileged functions without proper authorization checks. + +--- + +## The Core Issue + +1. **Arbitrary Call Data** + - The bridge contract receives arbitrary parameters (e.g., `toContract`, `method`, `args`) from user transactions or relayers. + - If these parameters aren’t adequately validated, the contract might call **itself** (or another sensitive contract) with escalated privileges. + +2. **Privileged Self-Calls** + - Some cross-chain manager contracts can act as an owner or admin for other contracts in the same system. + - If the manager can be tricked into calling itself with special permissions (e.g., `owner` functions), it effectively bypasses normal access controls. + +3. **Insufficient Checks** + - If the bridge only checks signature validity or message authenticity but **not** the actual function or contract being invoked, an attacker can slip in a call to update privileged roles, change deposit addresses, or seize funds. + +--- + +# Example: The Poly Network Hack + +On August 10, 2021, the Poly Network—an interoperability protocol linking multiple blockchains—suffered a major breach. By crafting **malicious call data**, the attacker was able to convince the protocol’s **cross-chain manager** to call privileged functions **on itself**, granting the attacker elevated permissions. This resulted in the theft of hundreds of millions of dollars in various cryptocurrencies across Ethereum, BSC, and Polygon. + +## Mitigation + +Strict Validation of Target and Methods. Deny self-calls to privileged functions unless explicitly intended. Only allow a whitelisted set of external contracts and method signatures to be invoked. + diff --git a/defi-security/crosschain_bridges/signature_replay/README.md b/defi-security/crosschain_bridges/signature_replay/README.md new file mode 100644 index 00000000..93802494 --- /dev/null +++ b/defi-security/crosschain_bridges/signature_replay/README.md @@ -0,0 +1,33 @@ +# Preventing Signature Replay Attacks in Multi-Chain Ecosystems + +When bridging assets between different blockchains, users typically sign messages or transactions that authorize their tokens to be transferred across networks. While this provides seamless interoperability, it also introduces a **signature replay** risk: a valid signature on one chain might be **reused** on another chain if no measures are taken to differentiate where the signature is supposed to be applied. + +--- + +## What Is a Signature Replay Attack? + +A signature replay attack occurs when: +1. **A user signs** a transaction or message on one blockchain (e.g., Ethereum Mainnet). +2. **The signature is still valid** on another chain (e.g., BNB Smart Chain or Polygon) because the system does not bind the signature to a specific network or context. +3. An attacker **reuses** this signature on the secondary chain, effectively duplicating the original transaction, which can lead to unauthorized asset movement or repeated token mints. + +--- + +## Example: Vulnerable Cross-Chain Bridge + +Let’s say Alice wants to move tokens from **Chain A** to **Chain B** . She creates a signature that authorizes the transfer of 100 tokens to a bridge contract. Once those tokens have been locked or burned on Chain A, an equivalent amount is minted for her on Chain B. However, if the bridge contract on **Chain C** (Polygon) also accepts the exact same signature without verifying that it is specific to Chain A, an attacker could take Alice’s signature and replay it on Polygon, minting another 100 tokens for free. + +### Attack Steps +1. **Alice Authorizes Transfer** + - Alice signs data indicating she is moving 100 tokens from Chain A to Chain B. +2. **Chain B Bridge Validates** + - The bridge on Chain B sees Alice’s valid signature and releases 100 tokens to her address. +3. **Signature Replay on Chain C** + - A malicious actor sends the same signature to the bridge on Chain C, which incorrectly treats it as a new request. + - Since the signature is accepted again, another 100 tokens are minted on Chain C—effectively duplicating the transaction. + +--- + +### Mitigation + +Incorporate the chain ID or a domain separator (e.g., using EIP-712) into the signed data. This way, a signature for chain ID 1 (Ethereum Mainnet) cannot be accepted on chain ID 56 (BNB Smart Chain). diff --git a/defi-security/crosschain_bridges/transfer_method/README.md b/defi-security/crosschain_bridges/transfer_method/README.md new file mode 100644 index 00000000..ad070a8d --- /dev/null +++ b/defi-security/crosschain_bridges/transfer_method/README.md @@ -0,0 +1,90 @@ +# Cross-Chain Bridge Vulnerability: Permanent Locking of Funds Using `transfer()` + +In cross-chain bridging, users often deposit tokens or ETH into a contract on one chain (e.g., Ethereum Layer 1) so that a corresponding representation can be minted on another chain (e.g., an L2 or sidechain). Eventually, users may wish to **redeem** or **withdraw** their tokens back to the original chain. However, some bridge implementations rely on the Solidity `transfer()` method when returning ETH to users. This can inadvertently lock funds **forever** if the recipient is a contract requiring more than 2300 gas in its `receive()` or `fallback()` function. + +--- + +## How This Affects Cross-Chain Bridges + +1. **Bridging and Redemption** + - In a typical cross-chain setup, a user sends ETH to a bridge contract on L1. + - After bridging, they might later request to withdraw or redeem that ETH back from the bridge. + +2. **Using `transfer()` in the Bridge** + - The bridge tries to finalize the redemption by `transfer()`-ing the user’s ETH back. + - If that user’s address is actually a contract—like a multisig wallet or a DeFi protocol—that needs more than 2300 gas in its `fallback()`/`receive()`, the withdrawal fails. + - The bridging contract’s redemption process **reverts**, and the user cannot retrieve their ETH. + +3. **Result: Funds Stuck in the Bridge** + - Because no other address is authorized to claim the same withdrawal, the ETH remains locked in the bridging contract. + - Future attempts to redeem the same balance also fail unless the code is modified or the bridging logic is upgraded (if at all possible). + +--- + +## Example: Bridge Withdrawals That Fail for Contract Addresses + +In this simplified `finalizeWithdrawal` function, the bridge tries to return ETH to the user (who might be a contract) using `transfer()`. The low gas stipend can cause a revert if the recipient’s fallback logic requires more gas. + +```solidity + // Called after a user has claimed tokens on L2 and wants to unlock them on L1 + function finalizeWithdrawal() external { + require(canWithdraw[msg.sender], "No withdrawal available"); + uint256 amount = balances[msg.sender]; + require(amount > 0, "Nothing to withdraw"); + + balances[msg.sender] = 0; + + // Vulnerable step: Using transfer() to push ETH + // If msg.sender is a contract that needs >2300 gas, this will revert + payable(msg.sender).transfer(amount); + + + balances[msg.sender] = 0; + canWithdraw[msg.sender] = false; + } + +``` + +Suppose a cross-chain bridge uses a queue-based redemption process: + +1. **User Deposits ETH** + - Alice sends 10 ETH from L1 to the Bridge contract to mint a wrapped token on L2. + +2. **Redemption Requested** + - Later, Alice wants to redeem her 10 ETH back to L1. + - She provides her **multisig** wallet address, which is stored in the bridge contract as the recipient. + +3. **Bridge Attempts `transfer()`** + - After a cooldown or waiting period, the bridge finalizes the redemption by doing: + ```solidity + payable(msg.sender).transfer(amount); + ``` + - However, the multisig’s `fallback()` requires more gas than `transfer()` provides (only 2300). + +4. **Transaction Reverts** + - Since the multisig contract can’t execute its logic with 2300 gas, the transaction fails. + - The bridging contract reverts the withdrawal, leaving the 10 ETH stuck in the bridge’s custody. + +5. **Assets Locked Forever** + - No alternative address can claim these funds, and Alice can’t modify her multisig to reduce gas usage. + - The 10 ETH remains trapped in the bridge contract indefinitely. + +--- + + +## Mitigations + +1. **Use `call` Instead of `transfer()`** + - A safer approach is: + ```solidity + (bool success, ) = payable(recipient).call{value: amount}(""); + require(success, "ETH transfer failed"); + ``` + - `call` can forward more gas, preventing the out-of-gas revert scenario. Make sure precautions are made against reentrancy. + +2. **Adopt Pull-Based Withdrawals** + - Instead of pushing ETH automatically, let recipients **pull** their funds. + - If a contract needs more gas, it can handle the withdrawal logic internally on its own terms. + + +By using more flexible methods to send ETH and accommodating higher gas requirements, cross-chain bridges can avoid permanently freezing user funds—particularly for advanced contract wallets on L1 or L2. diff --git a/defi-security/crosschain_bridges/unsafe_transferFrom/README.md b/defi-security/crosschain_bridges/unsafe_transferFrom/README.md new file mode 100644 index 00000000..c288d045 --- /dev/null +++ b/defi-security/crosschain_bridges/unsafe_transferFrom/README.md @@ -0,0 +1,28 @@ +# Missing Revert on Failed Token Transfers in Bridges + +When users deposit tokens into a cross-chain bridge, the typical process involves calling a token’s `transferFrom` function to move the user’s tokens into the bridge contract. The bridge then updates its internal records to reflect a successful deposit (e.g., incrementing a `balances[msg.sender]`). However, **not all tokens revert** upon transfer failure. Some tokens only return a boolean `false` without reverting the transaction. If a contract **ignores** this return value and assumes success, it can create a **false deposit** scenario where the user’s balance is updated even though **no tokens were actually transferred**. + +## Why It Happens +- **Non-Reverting Tokens** + Several tokens (like **BAT**, **HT**, **cUSDC**, **ZRX**) do **not** revert if `transferFrom` fails (e.g., due to insufficient funds). +- **Bridge Assumes Success** + The bridging contract might assume that if the call didn’t revert, the tokens were deposited successfully—never checking the boolean return value. +- **Incorrect Balance Accounting** + As a result, the bridge increments the user’s deposit balance even though **no tokens** arrived in the bridge contract. + +## Example: Vulnerable Bridge Deposit + +```solidity + function deposit(address token, uint256 amount) external { + // Vulnerable approach: ignoring the transferFrom's return value + IERC20NonReverting(token).transferFrom(msg.sender, address(this), amount); + + // Contract assumes deposit succeeded, updates user balance + deposits[token][msg.sender] += amount; + } + +``` + +## Mitigation + +Use safeTransferFrom. There are libraries that provide safe variants of transferFrom that revert on failure, eliminating silent failures. diff --git a/defi-security/erc4337/factory_create/README.md b/defi-security/erc4337/factory_create/README.md new file mode 100644 index 00000000..ed15f8a4 --- /dev/null +++ b/defi-security/erc4337/factory_create/README.md @@ -0,0 +1,79 @@ +# Vulnerability of Using `CREATE` Instead of `CREATE2` in ERC-4337 Factories + +In the **ERC-4337** (Account Abstraction) model, new smart contract wallets are typically deployed via a *factory* contract. If that factory relies on `CREATE` (`0xF0`) instead of `CREATE2` (`0xF5`), it introduces non-deterministic wallet addresses and potential vulnerabilities. ERC-4337 explicitly recommends that factories use `CREATE2`, returning **the same wallet address** every time the same parameters are used—even if the wallet is already deployed. + +--- + +## Why `CREATE` Is Problematic + +1. **Non-Deterministic Address Generation** + - `CREATE` bases the newly created contract’s address on the **factory’s nonce**. + - Each transaction that increments the factory’s nonce can change the final address, causing unpredictability and potential confusion. + +2. **Front-Running & Replay Attacks** + - Attackers can manipulate transaction ordering or intercept a user’s transaction to alter the factory’s nonce. + - This could lead to unexpected addresses, or let an attacker deploy a contract at a user’s intended address first. + +3. **Funding a Wallet That Doesn’t Exist** + - Without a **deterministic** address, users risk sending funds to an address that might never be deployed at all, resulting in lost or irretrievable tokens. + +4. **Lack of Reusable Addresses** + - If the wallet was already deployed at a given nonce, you can’t reliably re-derive that address to confirm if it’s the “same” wallet. + - Bundlers and other off-chain services can’t pre-compute addresses or check if an address is already deployed without complicated state checks on the factory’s nonce. + +--- + +## The `CREATE2` Solution + +1. **Deterministic Addressing** + - `CREATE2` calculates an address from: + - The **deployer** (factory) address + - A **salt** (user-specific data) + - The **creation code** + - The **bytecode** of the wallet + - Because of this, the wallet address is **independent** of the factory’s nonce or transaction ordering. + +2. **Counterfactual Deployment** + - Users can generate and share their wallet address **before** it’s actually deployed. + - Funds can be sent there immediately, and the wallet contract is deployed only when the user needs to interact. + +3. **Reproducible Address** + - If the same salt and code are provided again, `CREATE2` yields **the same** address. + - This means the factory can **return that address** even if it’s already been deployed, or if someone tries to deploy it again. + - It also allows bundlers to easily query the future address by simulating a call to `getSenderAddress()` without worrying if the contract is currently deployed. + +4. **ERC-4337 Compatibility** + - The EntryPoint contract expects wallet creation to be repeatable. + - If the wallet address is **already deployed**, the factory method should still return **that same** address. This enables services like bundlers to handle user operations seamlessly. + +--- + +## Returning the Same Address Even if Deployed + +One core ERC-4337 requirement is that the factory must **return the wallet address** even if the wallet has already been created. This is essential because: + +- **Bundlers** can safely simulate the wallet creation (via `initCode`) and get the address without knowing in advance whether the wallet is already deployed. +- **Idempotent Creation** ensures that repeated calls with the same parameters do not spawn multiple wallets but confirm or reveal the same address. + +### Example Scenario + +1. **User Operation** + - A user sends a `UserOperation` with `initCode` that points to the factory + constructor arguments. + - The EntryPoint calls the factory to deploy or confirm the wallet address. + +2. **Factory Using `CREATE2`** + - If the wallet for those parameters is **not** deployed yet, the factory calls `CREATE2` to deploy it and returns the new address. + - If it **is** already deployed, `CREATE2` with the same salt + code results in the **same** address. The factory returns that address again. + +3. **No Address Conflicts** + - The user or bundler can rely on the returned address being correct, regardless of nonce ordering or prior attempts. + +--- + +## Takeaways + +- **Using `CREATE`** can break the deterministic address property, which is integral to ERC-4337’s goal of “counterfactual” wallet creation. +- **`CREATE2`** offers **predictable**, **repeatable** deployments, letting the factory return the **same** address if it’s already been created with the same parameters. +- By adhering to these patterns, ERC-4337 wallets can seamlessly accept funds before actual deployment, minimize front-running risks, and simplify user onboarding. + +Ultimately, **CREATE2** is **not optional**—it’s essential for safe, deterministic account abstraction in an ERC-4337 world, ensuring that the factory can confirm or create a wallet at a stable address each time. diff --git a/defi-security/erc4337/paymaster/README.md b/defi-security/erc4337/paymaster/README.md new file mode 100644 index 00000000..c2678493 --- /dev/null +++ b/defi-security/erc4337/paymaster/README.md @@ -0,0 +1,40 @@ +# ERC-4337 Paymaster and Signature Replay Vulnerabilities + +When integrating **ERC-4337** (Account Abstraction) with **Paymasters**—contracts that sponsor gas for user operations (UserOps)—careful signature handling is essential. Improper validation can expose the Paymaster to **replay attacks** and **overpayment exploits**, undermining both the wallet’s and Paymaster’s security. + +--- + +## 1. Paymaster Signature Replay (Cross-Chain Risk) + +**Problem**: If a Paymaster’s signature over the UserOp does **not** include the `chainId` (or other chain-specific data) in its hashing scheme, an operation **may** be replayed on another chain with the same Paymaster signer. This is because the same UserOp data (including the Paymaster signature) could be valid across multiple chains if the addresses and verifying signer remain consistent. + +### Impact + +- **Cross-Chain Replay**: The user’s operation can be executed on multiple networks if the Paymaster’s signature does not distinguish chain contexts. +- **Sponsorship Drain**: Attackers can repeatedly sponsor the same UserOp on different chains, draining Paymaster funds. + +### Mitigation + +- **Include `chainId` in the Signature Hash** + - Incorporate the current chain ID into `getHash` (or similar function) so that a signature from one chain **cannot** be valid on another. + + +--- + +## 2. Missing Fields in Signed Data (tokenGasPrice) + +**Problem**: When a user’s wallet refunds gas in ERC20 tokens, certain fields (e.g `tokenGasPrice`) must be included in the signed data to prevent tampering by the transaction submitter. If that field is **omitted** from the hash, an attacker can override the factor after the user has already signed, inflating their gas reimbursement and stealing user funds. + +### Impact + +- **Overpayment of Gas Refund**: The sponsor or user paying for gas in ERC20 tokens loses significantly more tokens than expected. +- **Theft of Funds**: The attacker effectively “pays themselves” an inflated refund, draining the user’s account or the Paymaster’s balance. + +### Mitigation + +- **Include All Critical Fields in the Hash** + - The user’s signature must cover every parameter that affects payment, especially including token gas price. + +- **Validate Off-Chain** + - Signatures should represent the entire transaction, including all gas refund parameters, so the user knows exactly how much is at risk. + diff --git a/defi-security/erc4337/signature_validation/README.md b/defi-security/erc4337/signature_validation/README.md new file mode 100644 index 00000000..b363c420 --- /dev/null +++ b/defi-security/erc4337/signature_validation/README.md @@ -0,0 +1,67 @@ +# Why Signature Validation Is Crucial in ERC-4337 Wallets + +**ERC-4337** (Account Abstraction) allows smart contract wallets to handle user operations in a flexible, programmable way. However, this flexibility also introduces critical security responsibilities—particularly around **signature validation**. If your wallet contract fails to strictly verify signers, malicious actors could execute transactions without proper authorization, jeopardizing user funds and the contract’s integrity. + +--- + +## The Role of Signature Validation in ERC-4337 + +1. **User Operations over EOAs** + - Traditional accounts (EOAs) rely on Ethereum’s built-in signature scheme. + - Under ERC-4337, a wallet contract must implement its own logic to verify that the transaction is **truly** coming from the intended user. + +2. **Multiple Signature Schemes** + - ERC-4337 wallets can support both standard ECDSA-based signatures and smart-contract-based signatures (e.g., EIP-1271). + - This makes the signature validation function more versatile—but also more complex. + +3. **Programmable Access Control** + - You can permit multiple owners, different signing thresholds, or specialized “guard” checks. + - Each extension must ensure that only authorized signers can pass the validation step. + +--- + +## Consequences of Poor Validation + +1. **Unauthorized Transactions** + - Attackers could craft signatures that exploit incomplete checks—running arbitrary calls through your wallet without owner consent. + +2. **Fund Theft & Protocol Exploits** + - An insufficiently validated signature can allow malicious transfers, draining user funds or interacting with other protocols on behalf of the wallet. + +3. **Upgradeable Logic Compromises** + - If the wallet supports upgradeable implementations or dynamic modules, incorrect validation might let an attacker install malicious code. + +--- + +## Common Pitfalls + +1. **Forgetting to Confirm Ownership** + - EIP-1271 “magic value” checks merely confirm that a contract *claims* to validate the signature. + - You must still verify that this contract truly belongs to the user (e.g., `require(signer == owner)`). + +2. **Allowing Arbitrary Contract Signers** + - If your wallet code never restricts which contract can pass EIP-1271 checks, any contract returning the correct magic value can impersonate the user. + +3. **Missing Nonce or Replay Checks** + - Even a valid signature should be used only once. + - Failing to track nonces could allow replay attacks, letting the same operation be repeated over and over. + +--- + +## Best Practices for Secure Signature Validation + +1. **Bind Signer to Wallet Ownership** + - When EIP-1271 is detected, confirm that the contract signer's address is actually the **owner** (or a delegated address) of the wallet. + +2. **Implement Nonce Management** + - Maintain a per-user or per-batch nonce. + - Reject operations that use an already-consumed nonce. + +3. **Consider Time-Stamped or Expiration Fields** + - Optionally require signatures to include a timestamp or block limit, preventing indefinite signature validity. + +--- + +## Conclusion + +ERC-4337 offers powerful abstractions that replace the default EOA model with customizable smart contract wallets. Yet, this added freedom demands **stringent signature validation** to prevent unauthorized operations. By ensuring every signature is tied to the correct owner, implementing robust replay protections, and carefully reviewing all code paths where a signature is checked, you can preserve user trust and secure their assets in an ERC-4337 environment. diff --git a/defi-security/erc4337/userOp_signature/README.md b/defi-security/erc4337/userOp_signature/README.md new file mode 100644 index 00000000..f20a4376 --- /dev/null +++ b/defi-security/erc4337/userOp_signature/README.md @@ -0,0 +1,52 @@ +# Signature Validation and Return Codes in ERC-4337 + +To be compliant with EIP-4337 standards, under **ERC-4337**, user operations (UserOps) are validated by the account contract itself rather than relying on the built-in EOA signature mechanism. When an account does **not** implement signature aggregation, it must directly validate the UserOp’s signature. Specifically: + +1. **Hashing the UserOp** + - The account receives a `userOpHash`, which is a hash of: + - The user operation fields (excluding the signature). + - The `entryPoint` address. + - The `chainId`. + - This unique hash ensures that each UserOp is tied to a specific chain and entry point, preventing replay on different networks or entry points. + +2. **Signature Check** + - The account **must** confirm that the provided signature is valid **for** the `userOpHash`. + - If the signature is **invalid**, the contract should return a **special code** (`SIG_VALIDATION_FAILED`) rather than reverting the transaction. + +3. **Non-Revert on Signature Mismatch** + - **Why not revert?** + - Reverting prevents the bundler from distinguishing a genuine signature failure from a broader contract error. + - By returning `SIG_VALIDATION_FAILED`, the account signals to the bundler that the user’s signature is incorrect—so the operation can be rejected without incurring wasted gas or ambiguous error handling. + +4. **Other Errors Must Revert** + - If any error besides an invalid signature occurs (e.g., malformed UserOp data, out-of-gas conditions, unauthorized function calls), the contract **must** revert. + - This clear distinction between a “bad signature” (non-revert) and a “contract error” (revert) helps the bundler handle user operations more reliably. + +--- + +## Example Flow + +1. **Bundler** + - Gathers multiple UserOps into a batch. + - Calls `validateUserOp` on each user’s account. + +2. **Account Contract** + - Computes or receives the `userOpHash`. + - Validates the user’s signature: + ```solidity + if (!isValidSignature(userOpHash, signature)) { + return SIG_VALIDATION_FAILED; + } + ``` + - If the signature is valid, the function returns `0` (or another success indicator). If any other error arises (e.g., the contract can’t parse input), it reverts. + +3. **Bundler’s Response** + - If the account returns `SIG_VALIDATION_FAILED`, the bundler knows to drop this operation from the batch. + - If the account reverts, it indicates an internal error with the operation or the contract logic. + +--- + +## Key Takeaways + +- **No Signature Aggregation**: The account is responsible for direct signature verification. +- **Return Codes**: An **invalid signature** yields `SIG_VALIDATION_FAILED`, while **all other errors** must revert. diff --git a/defi-security/erc4337/wallet_hijacking/README.md b/defi-security/erc4337/wallet_hijacking/README.md new file mode 100644 index 00000000..4b44f20e --- /dev/null +++ b/defi-security/erc4337/wallet_hijacking/README.md @@ -0,0 +1,53 @@ +# Counterfactual Wallet Vulnerability: Attacker Can Deploy a Wallet With Arbitrary EntryPoint + +In **counterfactual wallet** setups, a user or dApp can generate a wallet address off-chain (before it's actually deployed on-chain), allowing them to receive funds in advance. Later, when needed, the user deploys the wallet code at that pre-generated address. + +However, if crucial parameters (like the wallet’s **entry point**) are **not** included in the deterministic address derivation (e.g., in the `salt` for `CREATE2`), an attacker can **front-run** the legitimate user’s deployment. By deploying the exact same counterfactual address but providing **their own** malicious entry point, the attacker gains control over that wallet. This lets them execute arbitrary calls—stealing funds or modifying ownership—before the real owner even deploys the wallet. + +--- + +## How the Attack Works + +1. **Counterfactual Address Generation** + - The project’s factory contract offers a function, e.g., `getAddressForCounterfactualWallet(owner, index)`, which uses `CREATE2` to compute a future wallet address based on `owner` and `index`. + - **Issue**: The computed address does **not** depend on the chosen `entryPoint`. + +2. **User Funds the Future Address** + - Confident in the counterfactual design, the user (or others) sends ETH or tokens to the wallet’s predicted address **before** the wallet is deployed. + +3. **Attacker Deploys First** + - Observing blockchain mempool or user behavior, the attacker calls the factory with the **same** `owner` and `index` but uses a **malicious** entry point. + - Because the derived wallet address only depends on `(owner, index)` and *not* the entry point, the resulting on-chain address is **the same** as the user’s counterfactual address. + +4. **Malicious Entry Point** + - The attacker’s custom entry point (e.g., `StealEntryPoint`) gives them complete control to invoke privileged functions like `execFromEntryPoint`. + - They drain any ETH or tokens already at that address (or continue performing other malicious operations). + +--- + +## Real-World Example + +Below is a snippet from a reported issue on a **SmartAccountFactory** (part of the Biconomy codebase). It shows the factory’s `deployCounterFactualWallet` using a salt derived only from `_owner` and `_index`: + +```solidity +function deployCounterFactualWallet(address _owner, address _entryPoint, address _handler, uint _index) + public returns(address proxy) +{ + bytes32 salt = keccak256(abi.encodePacked(_owner, address(uint160(_index)))); + + bytes memory deploymentData = abi.encodePacked(type(Proxy).creationCode, uint(uint160(_defaultImpl))); + + assembly { + proxy := create2(0x0, add(0x20, deploymentData), mload(deploymentData), salt) + } + + require(address(proxy) != address(0), "Create2 call failed"); + + emit SmartAccountCreated(proxy, _defaultImpl, _owner, VERSION, _index); + + // The `_owner` is set here, but `_entryPoint` is not used in the salt for address derivation + BaseSmartAccount(proxy).init(_owner, _entryPoint, _handler); + + isAccountExist[proxy] = true; +} +``` \ No newline at end of file diff --git a/defi-security/price_oracles/decimal_scaling/README.md b/defi-security/price_oracles/decimal_scaling/README.md new file mode 100644 index 00000000..02c9e823 --- /dev/null +++ b/defi-security/price_oracles/decimal_scaling/README.md @@ -0,0 +1,34 @@ +# Decimal Scaling Vulnerabilities + +## Overview + +Many smart contracts depend on external price oracles to obtain critical financial data. A common assumption is that these oracles always return values with 18 decimals. However, this is not guaranteed; different oracles may use a variable number of decimals. Relying on a fixed 18-decimal assumption can result in inaccurate computations, leading to mispriced transactions, incorrect reward calculations, and financial losses. + +--- + +## Vulnerability Details + +### Wrong Price Scaling +Some contracts, multiply values from two different Chainlink oracles: one for the price (in ETH) and another for the USD value per ETH. Often, the code assumes both oracle responses are scaled to 18 decimals: + +## Impact + +- **Inaccurate Calculations**: + Incorrect scaling can result in either overestimating or underestimating important financial metrics. + +- **Financial Losses**: + Users might be charged too much or too little, leading to loss of funds or improper fund allocation during transactions. + +--- + +## Recommended Mitigation Steps + +1. **Dynamic Decimal Adjustment** + - Query the oracle’s `decimals()` function to determine the actual number of decimals for each oracle response. + + - Avoid hardcoding decimal assumptions; instead, adjust calculations based on live data from the oracle. +--- + +## Conclusion + +Relying on a fixed 18-decimal assumption for oracle data can lead to severe vulnerabilities and operational inconsistencies. By dynamically adjusting for the actual decimals reported by oracles and standardizing scaling across your contract, you can ensure accurate price calculations and protect users from financial harm. diff --git a/defi-security/price_oracles/dex_spot_price/README.md b/defi-security/price_oracles/dex_spot_price/README.md new file mode 100644 index 00000000..c4b23aa7 --- /dev/null +++ b/defi-security/price_oracles/dex_spot_price/README.md @@ -0,0 +1,33 @@ +# DEX Spot Price Vulnerability + +## Overview + +Many protocols use decentralized exchange (DEX) spot prices to determine the value of assets within their systems. However, relying on spot prices—such as those provided directly by Uniswap—can be risky because these prices represent only a single moment in time and are highly susceptible to manipulation. This vulnerability can lead to incorrect pricing in critical functions, potentially exposing the protocol to financial losses and exploits. + +## Vulnerability Details + +- **Manipulation Risk**: + DEX spot prices can be easily influenced by low-liquidity trades, flash loan attacks, or other market manipulations. Since the spot price reflects the current state of the order book, an attacker can temporarily skew the price for their own benefit. + + +## Impact + +- **Financial Losses**: + Manipulated spot prices can cause protocols to overvalue or undervalue assets, leading to improper fee calculations, mismanaged collateral ratios, and ultimately, financial losses. + +- **Exploitation**: + Attackers may manipulate spot prices to trigger unfavorable conditions in a protocol, such as forced liquidations or unbalanced asset distributions, effectively exploiting the system. + + +## Recommended Mitigation Steps + +1. **Implement a Time-Weighted Average Price (TWAP) Oracle** + Instead of relying on a single spot price, protocols should use TWAP oracles that aggregate price data over a predetermined time period. This approach smooths out short-term volatility and provides a more stable and reliable price. + +2. **Utilize Cumulative Price Variables** + Uniswap, for instance, provides cumulative price data (e.g., `priceCumulativeLast`) that can be used to calculate TWAP. By using these variables, you can derive an average price that is less susceptible to momentary manipulation. + + +## Conclusion + +Relying solely on DEX spot prices for critical price data is inherently risky due to their susceptibility to manipulation and volatility. \ No newline at end of file diff --git a/defi-security/price_oracles/heartbeat/README.md b/defi-security/price_oracles/heartbeat/README.md new file mode 100644 index 00000000..36d7884a --- /dev/null +++ b/defi-security/price_oracles/heartbeat/README.md @@ -0,0 +1,36 @@ +Heartbeat Discrepancies + +## Overview + +When using Chainlink price feeds, many contracts depend on periodic updates—known as heartbeats—to keep on-chain prices current. However, if a contract relies on a fixed update interval without validating the freshness of the data, it risks using outdated prices. This can lead to exploitable discrepancies between the on-chain price and real market conditions, opening the door for arbitrage attacks. + +## Key Risks + +- **Delayed Price Updates**: + If an asset’s price fluctuates within a preset deviation threshold, the oracle might not update the on-chain price until the heartbeat interval elapses. This lag can result in using stale price data during critical operations. + +- **Arbitrage Opportunities**: + An attacker can exploit the difference between the outdated on-chain price and the current market price, profiting from the temporary mispricing. + +- **Inconsistent Data Validity**: + Relying solely on a fixed heartbeat without additional checks does not guarantee that the price reflects the latest market conditions, compromising the security and reliability of any dependent contract logic. + +## Enhanced Validation Approach + +Instead of depending on a fixed heartbeat, it is advisable to use Chainlink's `latestRoundData` function. This function returns not only the current price but also important metadata that can be used for further validation: + +- **roundId**: The unique identifier of the update round. +- **updateTime**: The timestamp when the price was last updated. +- **answeredInRound**: The round in which the price was determined. + +These extra parameters allow you to confirm that the data is both complete and recent. + +### Example Code + +```solidity +(uint80 roundId, int256 rawPrice, , uint256 updateTime, uint80 answeredInRound) = AggregatorV3Interface(oracleAddress).latestRoundData(); + +require(rawPrice > 0, "Price must be > 0"); +require(updateTime != 0, "Round incomplete"); +require(answeredInRound >= roundId, "Price data is stale"); +``` diff --git a/defi-security/price_oracles/latest_answer/README.md b/defi-security/price_oracles/latest_answer/README.md new file mode 100644 index 00000000..a0abd8eb --- /dev/null +++ b/defi-security/price_oracles/latest_answer/README.md @@ -0,0 +1,49 @@ +# Chainlink Price Oracle Validation: Use latestRoundData for Reliable Data + +## Overview + +Many smart contracts depend on Chainlink price feeds for accurate market data. However, some implementations still use the deprecated `latestAnswer` method to fetch prices. While `latestAnswer` returns the last recorded price, it lacks additional information that verifies the data's freshness and completeness. This absence of validation can lead to the use of stale or unreliable prices in critical operations. + +## Vulnerability Details + +- **Lack of Timeliness Verification**: + The `latestAnswer` method only provides the last price without any timestamp or round information. Without verifying that the data comes from a completed and recent round, contracts might use outdated prices. + +- **Silent Failures**: + If no valid answer is available, `latestAnswer` may return `0` without throwing an error. This can cause the contract logic to proceed with an invalid price. + +## Recommended Mitigation + +Instead of using `latestAnswer`, the recommended approach is to use the `latestRoundData` function. This method provides a richer set of data including: + +- **roundId**: The identifier for the price update round. +- **rawPrice**: The latest reported price. +- **updateTime**: The timestamp of the update. +- **answeredInRound**: The round in which the answer was computed. + +By incorporating additional checks on these values, you can ensure that the price is not only greater than zero but also current and from a complete round. + +### Example Implementation + +Replace your price feed call with the following code snippet: + +```solidity +(uint80 roundId, int256 rawPrice, , uint256 updateTime, uint80 answeredInRound) = Aggregator(oracleAddress).latestRoundData(); +require(rawPrice > 0, "Chainlink price <= 0"); +require(updateTime != 0, "Incomplete round"); +require(answeredInRound >= roundId, "Stale price"); +``` + +What These Checks Do: +``` +require(rawPrice > 0, "Chainlink price <= 0"): +``` +Ensures that the reported price is valid (non-zero). +``` +require(updateTime != 0, "Incomplete round"): +``` +Verifies that the round has been completed and the data is finalized. +``` +require(answeredInRound >= roundId, "Stale price"): +``` +Confirms that the answer comes from the current or a later round, ensuring the price data is up-to-date. \ No newline at end of file diff --git a/defi-security/staking/erc777_reentrancy/README.md b/defi-security/staking/erc777_reentrancy/README.md new file mode 100644 index 00000000..1ea06d45 --- /dev/null +++ b/defi-security/staking/erc777_reentrancy/README.md @@ -0,0 +1,55 @@ +# General Considerations for ERC777 Reentrancy Vulnerabilities + +ERC777 introduces **hooks**—a mechanism allowing recipients to register a function called automatically when they receive tokens. This added flexibility also creates a **reentrancy risk**, especially if a contract sends ERC777 tokens without carefully updating its state beforehand. Attackers can exploit these hooks to reenter vulnerable contract functions, potentially draining assets such as rewards or staked tokens. + +--- + +## How ERC777 Reentrancy Arises + +1. **Hooks on Transfer** + - When an ERC777 token is transferred, the recipient can register a “tokensReceived” hook. + - If the sending contract (e.g., a staking or reward contract) invokes `transfer` on an ERC777 token, the recipient’s hook is triggered during the transfer process. + +2. **Reentrant Calls** + - If the contract **does not** follow safe practices (like the checks-effects-interactions pattern), the hook can re-enter the contract’s state-changing functions. + - For example, an attacker can repeatedly claim rewards if the contract updates internal balances **after** transferring the tokens. + +3. **Multiple Claims or Withdrawals** + - By reentering, an attacker could call the same function that distributed rewards multiple times in a single transaction, or manipulate other logic (e.g., share calculations, deposit/withdraw flows). + +--- + +## Example: Vulnerable `claimRewards` Function + +Consider a staking contract that uses ERC777 tokens for distributing rewards. Below is a simplified version of how a reentrancy hole might appear: + +```solidity +function claimRewards(address user, IERC20[] memory _rewardTokens) external { + for (uint8 i = 0; i < _rewardTokens.length; i++) { + uint256 rewardAmount = accruedRewards[user][_rewardTokens[i]]; + + if (rewardAmount == 0) revert("Zero rewards"); + + // Vulnerability: Transfer occurs before resetting accruedRewards + _rewardTokens[i].transfer(user, rewardAmount); + + // The reward is reset only AFTER sending tokens + // Attackers can reenter at this point if _rewardTokens[i] is ERC777 + accruedRewards[user][_rewardTokens[i]] = 0; + + emit RewardsClaimed(user, _rewardTokens[i], rewardAmount); + } +} +``` + +## Mitigations + +Use the Checks-Effects-Interactions Pattern. Update internal state before performing any external calls (like an ERC777 transfer). +For example: +``` +uint256 rewardAmount = accruedRewards[user][token]; +accruedRewards[user][token] = 0; // clear first +token.transfer(user, rewardAmount); // then transfer +``` + +Or use Reentrancy Guards. ReentrancyGuard modifiers can prevent nested calls to the same function. diff --git a/defi-security/staking/inflation_attack/README.md b/defi-security/staking/inflation_attack/README.md new file mode 100644 index 00000000..94b0fa67 --- /dev/null +++ b/defi-security/staking/inflation_attack/README.md @@ -0,0 +1,80 @@ +# First Depositor Inflation Attack with ERC20 Tokens + +In some staking or liquidity pool contracts, the first depositor can gain a huge advantage if **no** tokens have been deposited yet (`totalShares == 0`). By combining **front-running** with a follow-up “donation” of tokens that **does not** mint additional shares, the attacker can inflate the total token balance without increasing `totalShares`. As a result, **subsequent depositors** receive virtually no shares for their deposits. + +--- + +## The Attack Sequence + +1. **First Depositor Spotting** + - A malicious user watches for a transaction indicating a new deposit into an empty pool. + - Before the honest user’s deposit is confirmed, the attacker quickly deposits **1 wei** (or some minimal amount) of the ERC20 token to become the actual first depositor. + +2. **Minting Shares at 1:1** + - Because `totalShares == 0`, the staking contract mints shares equal to the deposited amount (e.g., 1 share for 1 token). + - Now the attacker holds **almost all** the existing shares (since nobody else deposited yet). + +3. **Donating (Inflating) Tokens** + - Next, the attacker **directly transfers** a large number of tokens to the contract’s address (e.g., 100,000 tokens), **without** calling the deposit function. + - The contract’s token balance (`totalAssets`) is now huge, but `totalShares` remains the same (the donation didn’t mint new shares). + +4. **Honest User’s Deposit** + - When the honest user’s deposit is finally mined, the pool calculates how many shares to mint using: ``` minted = (amount * totalShares) / (totalAssets);``` + - Since `totalAssets` is enormous, the ratio is tiny, resulting in **zero** shares minted. + +5. **Attacker Gains** + - The attacker, owning 100% of the pool’s shares, can later **withdraw** to claim the all of the tokens, including those deposited by the honest user. + +--- + +## Example of a Vulnerable Contract + +Below is a simplified ERC20-based staking contract that illustrates how an attacker can exploit the first deposit and a subsequent “donation.” Note that this contract: + +- Tracks total shares in `totalShares`. +- Uses `balanceOf(address(this))` (the token balance in the contract) as `totalAssets`. +- Does **not** handle direct transfers to the contract that inflate its balance without minting new shares. + +```solidity + function deposit(uint256 amount) external { + require(amount > 0, "Deposit must be > 0"); + + stakingToken.safeTransferFrom(msg.sender, address(this), amount); + + uint256 totalAssets = stakingToken.balanceOf(address(this)); // current token balance in the contract + + uint256 minted; + if (totalShares == 0) { + // First depositor: 1:1 ratio + minted = amount; + } else { + + minted = (amount * totalShares) / totalAssets; + } + + sharesOf[msg.sender] += minted; + totalShares += minted; + + emit Deposit(msg.sender, amount, minted); + } + + function withdraw(uint256 shareAmount) external { + require(shareAmount > 0, "Withdraw must be > 0"); + require(shareAmount <= sharesOf[msg.sender], "Not enough shares"); + + uint256 totalAssets = stakingToken.balanceOf(address(this)); + + uint256 withdrawAmount = (shareAmount * totalAssets) / totalShares; + + sharesOf[msg.sender] -= shareAmount; + totalShares -= shareAmount; + + stakingToken.safeTransfer(msg.sender, withdrawAmount); + + emit Withdraw(msg.sender, withdrawAmount, shareAmount); + } +``` + +## Mitigation + +Seed the Pool at Deployment. Fund the contract with some initial tokens (ensuring totalShares != 0 from the start). This weakens the first depositor advantage. \ No newline at end of file diff --git a/defi-security/staking/rebase_frontrun/README.md b/defi-security/staking/rebase_frontrun/README.md new file mode 100644 index 00000000..c6f40ce2 --- /dev/null +++ b/defi-security/staking/rebase_frontrun/README.md @@ -0,0 +1,58 @@ +# Front-Running Rebase Attack (Stepwise Jump in Rewards) + +Some staking protocols periodically distribute rewards (often called a “rebase” or “batch reward”) to existing participants based on their share holdings. **Front-Running Rebase Attacks** occur when an attacker observes a large incoming reward, quickly deposits tokens to stake **just before** the reward arrives, and then withdraws after collecting a windfall. This results in **legitimate long-term stakers** losing a portion of the rewards they would otherwise have received. + +--- + +## How It Works + +1. **Normal Staking & Reward Distribution** + - Legitimate participants deposit tokens into a staking contract and receive proportional shares. + - Periodically (or whenever certain conditions are met), a reward is added to the staking contract. The contract’s total assets increase, inflating the value of each share. + +2. **Attacker’s Timing** + - By monitoring mempool transactions or the protocol’s on-chain signals, an attacker can detect when a reward is about to arrive. + - Right **before** the reward hits the contract, the attacker deposits an amount to mint new shares at the **pre-reward** ratio. + +3. **Rebase Event** + - A large reward is then deposited into the staking contract, boosting `totalAssets`. + - The attacker’s newly minted shares appreciate significantly, despite having staked only moments before. + +4. **Immediate Withdrawal** + - The attacker quickly redeems their shares, claiming an outsized portion of the newly added reward. + - Long-term stakers who were in the pool beforehand see their rewards diluted by the attacker’s late deposit. + +--- + +## Attack Impact on Legitimate Stakers + +- **Diluted Rewards** + The reward that long-term stakers expect to share among themselves is partially diverted to the attacker. +- **Reduced Incentive to Stake Long-Term** + Honest users who stake for extended periods find their earnings siphoned off by last-moment depositors. +- **Increased Volatility** + Large, last-minute deposits before known rebase events create sudden swings in share distribution. + +--- + +## Example of a Vulnerable Staking Contract + +Below is a simplified example in which the contract periodically receives a reward (via the `receiveReward()` function). The attacker can watch for this call, deposit just prior, and then withdraw afterward to reap unearned gains. + +```solidity + function receiveReward(uint256 rewardAmount) external { + // e.g., this might be called by an external contract or admin + bool success = stakingToken.transferFrom(msg.sender, address(this), rewardAmount); + require(success, "Reward transfer failed"); + + emit RewardReceived(rewardAmount); + // Notice: No new shares are minted. Existing shares are effectively more valuable. + } +``` + +## Mitigation + +Deposit Lock / Vesting: Impose a lock-up period for deposits. Users who just deposited cannot withdraw until after a certain time or after the next reward cycle passes.This ensures that someone who arrives right before the reward can’t immediately exit. + +Time-Weighted Rewards: Distribute rewards based on time staked rather than just instantaneous share balances. +Users who stake longer receive proportionally more, preventing short-term entrants from siphoning an outsized portion. diff --git a/defi-security/staking/recover_erc20/README.md b/defi-security/staking/recover_erc20/README.md new file mode 100644 index 00000000..7f52f281 --- /dev/null +++ b/defi-security/staking/recover_erc20/README.md @@ -0,0 +1,38 @@ +# Rugability from a Poorly Implemented `recoverERC20` in Staking Contracts + +Many staking contracts include a **recoverERC20** function, allowing the owner to retrieve tokens accidentally sent to the contract. If not carefully designed, this feature can be abused for a **rug pull**, where the owner can withdraw critical assets—such as reward tokens—at will. An even trickier scenario arises when certain tokens have **multiple entry points** or “double addresses,” enabling an owner to bypass naive checks and drain staked or reward tokens by referencing an alternate address for the same token. + +--- + +## How the Vulnerability Arises + +1. **Recover Function Without Restrictions** + - A simple `recoverERC20` might allow the contract owner to retrieve **any** ERC20 token from the contract, including staking or reward tokens. + - Example: + + ```solidity + function recoverERC20(address token, uint256 amount) external onlyOwner { + // Vulnerable: no check against the token in use for staking or rewards + IERC20(token).transfer(msg.sender, amount); + } + +2. **Insufficient Checks** + +Some developers add a rudimentary check: require(token != address(stakingToken)) to prevent recovering the staked token. + +```solidity + function recoverERC20(address token, uint256 amount) external onlyOwner { + require(token != address(stakingToken)) + IERC20(token).transfer(msg.sender, amount); + } +``` +However, if a token has multiple contract addresses or a “double entry point” design, a cunning owner (or attacker) can pass a different address that references the same underlying token. +This effectively evades the naive token != stakedToken comparison. + +Because recoverERC20 can be called at any time by the owner, the entire reward pool or staked tokens can be siphoned out without user consent. + +## Mitigation Strategies + +Maintain a whitelist or blacklist of token addresses. Ensure the staked token, reward token, or any known “wrapper” addresses cannot be recovered. + +Or by entirely removing the ability to recover ERC20 tokens, developers can eliminate a major rug pull vector and maintain user confidence in the staking platform. \ No newline at end of file