├── vulnerabilities ├── unsecure-signatures.md ├── img │ ├── secp256k1.png │ └── elliptic-curves.png ├── outdated-compiler-version.md ├── unused-variables.md ├── floating-pragma.md ├── assert-violation.md ├── shadowing-state-variables.md ├── uninitialized-storage-pointer.md ├── requirement-violation.md ├── missing-protection-signature-replay.md ├── inadherence-to-standards.md ├── arbitrary-storage-location.md ├── use-of-deprecated-functions.md ├── incorrect-inheritance-order.md ├── asserting-contract-from-code-size.md ├── lack-of-precision.md ├── incorrect-constructor.md ├── off-by-one.md ├── default-visibility.md ├── transaction-ordering-dependence.md ├── insufficient-access-control.md ├── unexpected-ecrecover-null-address.md ├── insufficient-gas-griefing.md ├── timestamp-dependence.md ├── authorization-txorigin.md ├── unencrypted-private-data-on-chain.md ├── msgvalue-loop.md ├── signature-malleability.md ├── weak-sources-randomness.md ├── unsafe-low-level-call.md ├── delegatecall-untrusted-callee.md ├── unsupported-opcodes.md ├── dos-gas-limit.md ├── unchecked-return-values.md ├── overflow-underflow.md ├── dos-revert.md ├── unbounded-return-data.md ├── reentrancy.md └── hash-collision.md ├── .github ├── issue_template.md ├── pull_request_template.md └── workflows │ └── deploy-ghpages.yml ├── LICENSE ├── README.md └── style-guide.md /vulnerabilities/unsecure-signatures.md: -------------------------------------------------------------------------------- 1 | ## Unsecure Signatures 2 | 3 | -------------------------------------------------------------------------------- /vulnerabilities/img/secp256k1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kadenzipfel/smart-contract-vulnerabilities/HEAD/vulnerabilities/img/secp256k1.png -------------------------------------------------------------------------------- /vulnerabilities/img/elliptic-curves.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kadenzipfel/smart-contract-vulnerabilities/HEAD/vulnerabilities/img/elliptic-curves.png -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | ## Checklist 2 | - [ ] I have searched the existing issues and pull requests for duplicates. 3 | 4 | ## Type of Issue 5 | - [ ] New vulnerability addition 6 | - [ ] Feature request 7 | - [ ] Update existing vulnerability 8 | 9 | ## Description 10 | 11 | 12 | ## Additional Information 13 | 14 | -------------------------------------------------------------------------------- /vulnerabilities/outdated-compiler-version.md: -------------------------------------------------------------------------------- 1 | ## Outdated Compiler Version 2 | 3 | Developers often find bugs and vulnerabilities in existing software and make patches. For this reason, it's important to use the most recent compiler version possible. See bugs from past compiler versions [here](https://solidity.readthedocs.io/en/latest/bugs.html). 4 | 5 | ### Sources 6 | 7 | - [SWC-102](https://swcregistry.io/docs/SWC-102) 8 | - [Ethereum Solidity Releases](https://github.com/ethereum/solidity/releases) 9 | - [Etherscan Solidity Bug Info](https://etherscan.io/solcbuginfo) 10 | - [Solidity Documentation - Bugs](https://solidity.readthedocs.io/en/latest/bugs.html) -------------------------------------------------------------------------------- /vulnerabilities/unused-variables.md: -------------------------------------------------------------------------------- 1 | ## Presence of Unused Variables 2 | 3 | Although it is allowed, it is best practice to avoid unused variables. Unused variables can lead to a few different problems: 4 | 5 | - Increase in computations (unnecessary gas consumption) 6 | - Indication of bugs or malformed data structures 7 | - Decreased code readability 8 | 9 | It is highly recommended to remove all unused variables from a code base. 10 | 11 | ### Sources 12 | 13 | - [SWC-131: Missing Protection Against Signature Replay Attacks](https://swcregistry.io/docs/SWC-131) 14 | - [Solidity Issue #718](https://github.com/ethereum/solidity/issues/718) 15 | - [Solidity Issue #2563](https://github.com/ethereum/solidity/issues/2563) -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Related Issue 2 | 3 | - Closes: (Link the issue this pull request addresses) 4 | 5 | ## Checklist 6 | 7 | - [ ] I have read and followed the [style guide](https://github.com/kadenzipfel/smart-contract-vulnerabilities/blob/master/style-guide.md) 8 | 9 | ### Describe the changes you've made: 10 | 11 | Explain how your contribution adds the vulnerability information to the repository. 12 | Mention if a new category or tag is needed for this vulnerability type. 13 | 14 | ## Type of change 15 | 16 | Select the appropriate checkbox: 17 | 18 | - [ ] Bug fix (fixing an issue with existing vulnerability data) 19 | - [ ] New feature (adding a new vulnerability or category) 20 | - [ ] Documentation update (improving existing information) 21 | 22 | ## Additional Information 23 | 24 | Mention any specific considerations or limitations of your contribution. 25 | -------------------------------------------------------------------------------- /vulnerabilities/floating-pragma.md: -------------------------------------------------------------------------------- 1 | ## Floating Pragma 2 | 3 | It is considered best practice to pick one compiler version and stick with it. With a floating pragma, contracts may accidentally be deployed using an outdated or problematic compiler version which can cause bugs, putting your smart contract's security in jeopardy. For open-source projects, the pragma also tells developers which version to use, should they deploy your contract. The chosen compiler version should be thoroughly tested and considered for known bugs. 4 | 5 | The exception in which it is acceptable to use a floating pragma, is in the case of libraries and packages. Otherwise, developers would need to manually update the pragma to compile locally. 6 | 7 | ### Sources 8 | 9 | - [SWC-103](https://swcregistry.io/docs/SWC-103) 10 | - [Consensys Smart Contract Best Practices - Locking Pragmas](https://consensys.github.io/smart-contract-best-practices/development-recommendations/solidity-specific/locking-pragmas/) -------------------------------------------------------------------------------- /vulnerabilities/assert-violation.md: -------------------------------------------------------------------------------- 1 | ## Assert Violation 2 | 3 | In Solidity `0.4.10`, the following functions were created: `assert()`, `require()`, and `revert()`. Here we'll discuss the assert function and how to use it. 4 | 5 | Formally said, the `assert()` function is meant to assert invariants; informally said, `assert()` is an overly assertive bodyguard that protects your contract, but steals your gas in the process. Properly functioning contracts should never reach a failing assert statement. If you've reached a failing assert statement, you've either improperly used `assert()`, or there is a bug in your contract that puts it in an invalid state. 6 | 7 | If the condition checked in the `assert()` is not actually an invariant, it's suggested that you replace it with a `require()` statement. 8 | 9 | ### Sources 10 | 11 | - [SWC-110](https://swcregistry.io/docs/SWC-110) 12 | - [The Use of revert, assert, and require in Solidity and the New REVERT Opcode in the EVM](https://medium.com/blockchannel/the-use-of-revert-assert-and-require-in-solidity-and-the-new-revert-opcode-in-the-evm-1a3a7990e06e) 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 kadenzipfel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /vulnerabilities/shadowing-state-variables.md: -------------------------------------------------------------------------------- 1 | ## Shadowing State Variables 2 | 3 | It is possible to use the same variable twice in Solidity, but it can lead to unintended side effects. This is especially difficult regarding working with multiple contracts. Take the following example: 4 | 5 | ```solidity 6 | contract SuperContract { 7 | uint a = 1; 8 | } 9 | 10 | contract SubContract is SuperContract { 11 | uint a = 2; 12 | } 13 | ``` 14 | 15 | Here we can see that `SubContract` inherits `SuperContract` and the variable `a` is defined twice with different values. Now say we use `a` to perform some function in `SubContract`, functionality inherited from `SuperContract` will no longer work since the value of `a` has been modified. 16 | 17 | To avoid this vulnerability, it's important we check the entire smart contract system for ambiguities. It's also important to check for compiler warnings, as they can flag these ambiguities so long as they're in the smart contract. 18 | 19 | ### Sources 20 | 21 | - [SWC-119](https://swcregistry.io/docs/SWC-119) 22 | - [Solidity Issue #2563](https://github.com/ethereum/solidity/issues/2563) 23 | - [Solidity Issue #973](https://github.com/ethereum/solidity/issues/973) -------------------------------------------------------------------------------- /vulnerabilities/uninitialized-storage-pointer.md: -------------------------------------------------------------------------------- 1 | ## Uninitialized Storage Pointer 2 | 3 | > [!NOTE] 4 | > As of solidity `0.5.0`, uninitialized storage pointers are no longer an issue since contracts with uninitialized storage pointers will no longer compile. This being said, it's still important to understand what storage pointers you should be using in certain situations. 5 | 6 | Data is stored in the EVM as either `storage`, `memory`, or `calldata`. It is important that they are well understood and correctly initialized. Incorrectly initializing data storage pointers, or simply leaving them uninitialized, can lead to contract vulnerabilities. 7 | 8 | ### Sources 9 | 10 | - [SWC-109: Arbitrary Storage Write](https://swcregistry.io/docs/SWC-109) 11 | - [Solidity Security Blog - Storage](https://github.com/sigp/solidity-security-blog#storage) 12 | - [Solidity Documentation: Data Location](https://solidity.readthedocs.io/en/latest/types.html#data-location) 13 | - [Solidity Documentation: Layout in Storage](https://docs.soliditylang.org/en/latest/internals/layout_in_storage.html) 14 | - [Solidity Documentation: Layout in Memory](https://docs.soliditylang.org/en/latest/internals/layout_in_memory.html) 15 | -------------------------------------------------------------------------------- /vulnerabilities/requirement-violation.md: -------------------------------------------------------------------------------- 1 | ## Requirement Violation 2 | 3 | The `require()` method is meant to validate conditions, such as inputs or contract state variables, or to validate return values from external contract calls. For validating external calls, inputs can be provided by callers, or they can be returned by callees. In the case that an input violation has occurred by the return value of a callee, likely one of two things has gone wrong: 4 | 5 | - There is a bug in the contract that provided the input. 6 | - The requirement condition is too strong. 7 | 8 | To solve this issue, first consider whether the requirement condition is too strong. If necessary, weaken it to allow any valid external input. If the problem isn't the requirement condition, there must be a bug in the contract providing external input. Ensure that this contract is not providing invalid inputs. 9 | 10 | ### Sources 11 | 12 | - [SWC-123: Requirement Violation (SWC Registry)](https://swcregistry.io/docs/SWC-123) 13 | - [The Use of revert, assert, and require in Solidity, and the New REVERT Opcode in the EVM](https://medium.com/blockchannel/the-use-of-revert-assert-and-require-in-solidity-and-the-new-revert-opcode-in-the-evm-1a3a7990e06e) 14 | -------------------------------------------------------------------------------- /vulnerabilities/missing-protection-signature-replay.md: -------------------------------------------------------------------------------- 1 | ## Missing Protection against Signature Replay Attacks 2 | 3 | Sometimes in smart contracts it is necessary to perform signature verification to improve usability and gas cost. However, consideration needs to be taken when implementing signature verification. To protect against Signature Replay Attacks, the contract should only be allowing new hashes to be processed. This prevents malicious users from replaying another users signature multiple times. 4 | 5 | To be extra safe with signature verification, follow these recommendations: 6 | 7 | - Store every message hash processed by the contract, then check messages hashes against the existing ones before executing the function. 8 | - Include the address of the contract in the hash to ensure that the message is only used in a single contract. 9 | - Never generate the message hash including the signature. See [Signature Malleability](./signature-malleability.md) 10 | 11 | ### Sources 12 | 13 | - [SWC-121](https://swcregistry.io/docs/SWC-121) 14 | - [Medium - Replay Attack Vulnerability in Ethereum Smart Contracts](https://medium.com/cypher-core/replay-attack-vulnerability-in-ethereum-smart-contracts-introduced-by-transferproxy-124bf3694e25) -------------------------------------------------------------------------------- /vulnerabilities/inadherence-to-standards.md: -------------------------------------------------------------------------------- 1 | ## Inadherence to Standards 2 | 3 | In terms of smart contract development, it's important to follow standards. Standards are set to prevent vulnerabilities, and ignoring them can lead to unexpected effects. 4 | 5 | Take for example binance's original BNB token. It was marketed as an ERC20 token, but it was later pointed out that it wasn't actually ERC20 compliant for a few reasons: 6 | 7 | - It prevented sending to 0x0 8 | - It blocked transfers of 0 value 9 | - It didn't return true or false for success or fail 10 | 11 | The main cause for concern with this improper implementation is that if it is used with a smart contract that expects an ERC-20 token, it will behave in unexpected ways. It could even get locked in the contract forever. 12 | 13 | Although standards aren't always perfect, and may someday become antiquated, they foster proper expectations to provide for secure smart contracts. 14 | 15 | Suggested by: [RobertMCForster](https://github.com/RobertMCForster) 16 | 17 | ### Sources 18 | 19 | - [BNB: Is It Really an ERC-20 Token?](https://finance.yahoo.com/news/bnb-really-erc-20-token-160013314.html) 20 | - [Binance Isn't ERC-20](https://blog.goodaudience.com/binance-isnt-erc-20-7645909069a4) -------------------------------------------------------------------------------- /vulnerabilities/arbitrary-storage-location.md: -------------------------------------------------------------------------------- 1 | ## Write to Arbitrary Storage Location 2 | 3 | Only authorized addresses should have access to write to sensitive storage locations. If there isn't proper authorization checks throughout the contract, a malicious user may be able to overwrite sensitive data. However, even if there are authorization checks for writing to sensitive data, an attacker may still be able to overwrite the sensitive data via insensitive data. This could give an attacker access to overwrite important variables such as the contract owner. 4 | 5 | To prevent this from occurring, we not only want to protect sensitive data stores with authorization requirements, but we also want to ensure that writes to one data structure cannot inadvertently overwrite entries of another data structure. 6 | 7 | For an example, try [Ethernaut - Alien Codex](https://ethernaut.openzeppelin.com/level/19). If it's too hard, see [this walkthrough (SPOILER)](https://github.com/theNvN/ethernaut-openzeppelin-hacks/blob/main/level_19_Alien-Codex.md). 8 | 9 | ### Sources 10 | 11 | - [SWC-124: Write to Arbitrary Storage Location](https://swcregistry.io/docs/SWC-124) 12 | - [USCC 2017 Submission by doughoyte](https://github.com/Arachnid/uscc/tree/master/submissions-2017/doughoyte) 13 | -------------------------------------------------------------------------------- /vulnerabilities/use-of-deprecated-functions.md: -------------------------------------------------------------------------------- 1 | ## Use of Deprecated Functions 2 | 3 | As time goes by, functions and operators in Solidity are deprecated and often replaced. It's important to not use deprecated functions, as it can lead to unexpected effects and compilation errors. 4 | 5 | Here is a *non-exhaustive* list of deprecated functions and alternatives. Many alternatives are simply aliases, and won't break current behaviour if used as a replacement for its deprecated counterpart. 6 | 7 | | Deprecated | Alternatives | 8 | | :---------------------- | ------------------------: | 9 | | `suicide(address)`/`selfdestruct(address)` | N/A | 10 | | `block.blockhash(uint)` | `blockhash(uint)` | 11 | | `sha3(...)` | `keccak256(...)` | 12 | | `callcode(...)` | `delegatecall(...)` | 13 | | `throw` | `revert()` | 14 | | `msg.gas` | `gasleft` | 15 | | `constant` | `view` | 16 | | `var` | `corresponding type name` | 17 | 18 | ### Sources 19 | 20 | - [SWC-111: Use of Deprecated Solidity Features](https://swcregistry.io/docs/SWC-111) 21 | - [Solidity Releases on GitHub](https://github.com/ethereum/solidity/releases) 22 | -------------------------------------------------------------------------------- /vulnerabilities/incorrect-inheritance-order.md: -------------------------------------------------------------------------------- 1 | ## Incorrect Inheritance Order 2 | 3 | In Solidity, it is possible to inherit from multiple sources, which if not properly understood can introduce ambiguity. This ambiguity is known as the Diamond Problem, wherein if two base contracts have the same function, which one should be prioritized? Luckily, Solidity handles this problem gracefully, that is as long as the developer understands the solution. 4 | 5 | The solution Solidity provides to the Diamond Problem is by using reverse C3 linearization. This means that it will linearize the inheritance from right to left, so the order of inheritance matters. It is suggested to start with more general contracts and end with more specific contracts to avoid problems. 6 | 7 | ### Sources 8 | 9 | - [Consensys: Smart Contract Best Practices - Complex Inheritance](https://consensys.github.io/smart-contract-best-practices/development-recommendations/solidity-specific/complex-inheritance/) 10 | - [Solidity Documentation: Multiple Inheritance and Linearization](https://solidity.readthedocs.io/en/v0.4.25/contracts.html#multiple-inheritance-and-linearization) 11 | - [Wikipedia: The Diamond Problem](https://en.wikipedia.org/wiki/Multiple_inheritance#The_diamond_problem) 12 | - [Wikipedia: C3 Linearization](https://en.wikipedia.org/wiki/C3_linearization) 13 | -------------------------------------------------------------------------------- /vulnerabilities/asserting-contract-from-code-size.md: -------------------------------------------------------------------------------- 1 | ## Asserting contract from Code Size 2 | 3 | A common method for asserting whether a sender is a contract or EOA has been to check the code size of the sender. This check asserts that if the sender has a code size > 0 that it must be a contract and if not then it must be an EOA. For example: 4 | 5 | ```solidity 6 | function mint(uint256 amount) public { 7 | if (msg.sender.code.length != 0) revert CallerNotEOA(); 8 | } 9 | ``` 10 | 11 | However, as noted in the [Ethereum Yellow Paper](https://ethereum.github.io/yellowpaper/paper.pdf), "During initialization code execution, EXTCODESIZE on the address should return zero, which is the length of the code of the account while CODESIZE should return the length of the initialization". 12 | 13 | [This repo](https://github.com/0xKitsune/Ghost-Contract/blob/main/src/Ghost.sol) shows how we may exploit this logic by simply calling during creation of a new contract. 14 | 15 | As we can see, it's important that we recognize that although we may be certain that an account with a non-zero codesize is a contract, we can't be certain that an account with a zero codesize is not a contract. 16 | 17 | ### Sources 18 | 19 | - [Ethereum Yellow Paper](https://ethereum.github.io/yellowpaper/paper.pdf) 20 | - [Ghost Contract on GitHub](https://github.com/0xKitsune/Ghost-Contract) 21 | -------------------------------------------------------------------------------- /vulnerabilities/lack-of-precision.md: -------------------------------------------------------------------------------- 1 | ## Lack of Precision 2 | 3 | In Solidity, there are a limited variety of number types. Differently from many programming languages, floating point numbers are unsupported. Fixed point numbers are partially supported, but cannot be assigned to or from. The primary number type in Solidity are integers, of which resulting values of calculations are always rounded down. 4 | 5 | Since division often results in a remainder, performing division with integers generally requires a lack of precision to some degree. To see how a lack of precision may cause a serious flaw, consider the following example in which we charge a fee for early withdrawals denominated in the number of days early that the withdrawal is made: 6 | 7 | ```solidity 8 | uint256 daysEarly = withdrawalsOpenTimestamp - block.timestamp / 1 days 9 | uint256 fee = amount / daysEarly * dailyFee 10 | ``` 11 | 12 | The problem with this is that in the case that a user withdraws 1.99 days early, since 1.99 will round down to 1, the user only pays about half the intended fee. 13 | 14 | In general, we should ensure that numerators are sufficiently larger than denominators to avoid precision errors. A common solution to this problem is to use fixed point logic, i.e. raising integers to a sufficient number of decimals such that the lack of precision has minimal effect on the contract logic. A good rule of thumb is to raise numbers to 1e18 (commonly referred to as WAD). -------------------------------------------------------------------------------- /vulnerabilities/incorrect-constructor.md: -------------------------------------------------------------------------------- 1 | ## Incorrect Constructor Name 2 | 3 | > [!NOTE] 4 | > This vulnerability is relevant to older contracts using Solidity versions before `0.4.22`. Modern Solidity versions (0.4.22 and later) use the `constructor` keyword, effectively deprecating this vulnerability. However, it is still important to be aware of this issue when reviewing or interacting with legacy contracts. 5 | 6 | Before Solidity `0.4.22`, the only way to define a constructor was by creating a function with the contract name. In some cases this was problematic. For example, if a smart contract is re-used with a different name but the constructor function isn't also changed it simply becomes a regular, callable function. Similarly, it's possible for an attacker to create a contract with which a function appears to be the constructor but actually has one character replaced with a similar looking character, e.g. replacing an "l" with a "1", allowing logic to be executed when it's only expected to be executed during contract creation. 7 | 8 | Now with modern versions of Solidity, the constructor is defined with the `constructor` keyword, effectively deprecating this vulnerability. Thus the solution to this problem is simply to use modern Solidity compiler versions. 9 | 10 | ### Sources 11 | 12 | - [SWC-118](https://swcregistry.io/docs/SWC-118) 13 | - [Sigma Prime Blog - Solidity Security Constructors](https://blog.sigmaprime.io/solidity-security.html#constructors) -------------------------------------------------------------------------------- /vulnerabilities/off-by-one.md: -------------------------------------------------------------------------------- 1 | ## Off-By-One 2 | 3 | Off-by-one errors are a common mistake made by programmers in which the intended boundaries are incorrect by only one, though these errors may seem insignificant, the effect can easily be quite severe. 4 | 5 | ### Array lengths 6 | 7 | Properly determining intended array lengths is a common source of off-by-one errors. Particularly since 0-indexing means the final value in an array is `array.length - 1`. 8 | 9 | Consider for example a function intended to loop over a list of recipients to transfer funds to each user, but the loop length is incorrectly set. 10 | 11 | ```solidity 12 | // Incorrectly sets upper bound to users.length - 1 13 | // Final user in array doesn't receive token transfer 14 | for (uint256 i; i < users.length - 1; ++i) { 15 | token.transfer(users[i], 1 ether); 16 | } 17 | ``` 18 | 19 | ### Incorrect comparison operator 20 | 21 | It's common for comparison operators to be off by one when, e.g. `>` should be used in place of `>=`. This is especially common when the logic includes some kind of negation, leading to mental friction in deciphering the intended vs implemented bounds. 22 | 23 | Consider for example a Defi protocol with liquidation logic documented to liquidate a user only if their collateralization ratio is *below* 1e18. 24 | 25 | ```solidity 26 | // Incorrectly liquidates if collateralizationRatio is == 1 ether 27 | if (collateralizationRatio > 1 ether) { 28 | ... 29 | } else { 30 | liquidate(); 31 | } 32 | ``` 33 | 34 | ### Sources 35 | 36 | - [OpenCoreCH - Smart Contract Auditing Heuristics: Off-by-One Errors](https://github.com/OpenCoreCH/smart-contract-auditing-heuristics#off-by-one-errors) -------------------------------------------------------------------------------- /vulnerabilities/default-visibility.md: -------------------------------------------------------------------------------- 1 | ## Default Visibility 2 | 3 | Visibility specifiers are used to determine where a function or variable can be accessed from. As explained in the [solidity docs](https://docs.soliditylang.org/en/v0.8.15/cheatsheet.html?highlight=visibility#function-visibility-specifiers): 4 | 5 | - `public`: visible externally and internally (creates a [getter function](https://docs.soliditylang.org/en/v0.8.15/contracts.html#getter-functions) for storage/state variables) 6 | - `private`: only visible in the current contract 7 | - `external`: only visible externally (only for functions) - i.e. can only be message-called (via `this.func`) 8 | - `internal`: only visible internally 9 | 10 | It's important to note that the default visibility is `public`, allowing access externally or internally by any contract or EOA. We can see how this may be a problem if a method is intended to only be accessible internally but is missing a visibility specifier. 11 | 12 | Modern compilers should catch missing function visibility specifiers, but will generally allow missing state variable visibility specifiers. Regardless, it's important to be aware of the possible interactions which may occur as a result of default visibility specifiers for both functions and state variables. 13 | 14 | ### Sources 15 | 16 | - [SWC-100](https://swcregistry.io/docs/SWC-100) 17 | - [SWC-108](https://swcregistry.io/docs/SWC-108) 18 | - [Consensys Smart Contract Best Practices - Visibility](https://consensys.github.io/smart-contract-best-practices/development-recommendations/solidity-specific/visibility/) 19 | - [SigP Solidity Security Blog - Visibility](https://github.com/sigp/solidity-security-blog#visibility) -------------------------------------------------------------------------------- /vulnerabilities/transaction-ordering-dependence.md: -------------------------------------------------------------------------------- 1 | ## Transaction-Ordering Dependence 2 | 3 | Transactions on Ethereum are grouped together in blocks which are processed on a semi-regular interval, 12 seconds. Before transactions are placed in blocks, they are broadcasted to the mempool where block builders can then proceed to place them as is economically optimal. What's important to understand here is that the mempool is public and thus anyone can see transactions before they're executed, giving them the power to frontrun by placing their own transaction executing the same, or a similar, action with a higher gas price. 4 | 5 | Frontrunning has become prevalent as a result of generalized frontrunning bots becoming more and more common. These generalized frontrunners work by observing the mempool for profitable, replicable transactions which they can replace for their own benefit. [Ethereum is a Dark Forest](https://www.paradigm.xyz/2020/08/ethereum-is-a-dark-forest). 6 | 7 | One solution to transaction-ordering dependence is to use a commit-reveal scheme in the case of information being submitted on-chain. This works by having the submitter send in a hash of the information, storing that on-chain along with the user address so that they may later reveal the answer along with the salt to prove that they were indeed correct. Another solution is to simply use a private mempool such as [Flashbots](https://www.flashbots.net/). 8 | 9 | ### Sources 10 | 11 | - [Solidity Transaction Ordering Attacks](https://medium.com/coinmonks/solidity-transaction-ordering-attacks-1193a014884e) 12 | - [Analysis of Transaction Ordering in Ethereum](https://users.encs.concordia.ca/~clark/papers/2019_wtsc_front.pdf) 13 | - [SWC-114: Transaction Order Dependence](https://swcregistry.io/docs/SWC-114) 14 | -------------------------------------------------------------------------------- /.github/workflows/deploy-ghpages.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Main to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | permissions: 9 | contents: write 10 | pages: write 11 | 12 | jobs: 13 | build-deploy: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout main branch 18 | uses: actions/checkout@v3 19 | 20 | - name: Set up Rust 21 | uses: actions-rs/toolchain@v1 22 | with: 23 | toolchain: stable 24 | override: true 25 | 26 | - name: Install mdBook 27 | run: cargo install mdbook 28 | 29 | - name: Prepare src directory 30 | run: | 31 | mkdir -p src/vulnerabilities 32 | cp README.md src/README.md 33 | echo "[Introduction](README.md)" > src/SUMMARY.md 34 | cat README.md >> src/SUMMARY.md 35 | cp -r vulnerabilities/* src/vulnerabilities/ 36 | 37 | - name: Build the book 38 | run: mdbook build 39 | 40 | - name: Checkout gh-pages branch 41 | uses: actions/checkout@v3 42 | with: 43 | ref: gh-pages 44 | path: gh-pages 45 | 46 | - name: Copy built book to gh-pages 47 | run: | 48 | rm -rf gh-pages/docs 49 | mkdir -p gh-pages/docs 50 | cp -r book/* gh-pages/docs/ 51 | 52 | - name: Commit and push changes 53 | env: 54 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 55 | run: | 56 | cd gh-pages 57 | git config --global user.name 'github-actions[bot]' 58 | git config --global user.email 'github-actions[bot]@users.noreply.github.com' 59 | if [ -n "$(git status --porcelain)" ]; then 60 | git add docs 61 | git commit -m 'Deploy new changes' 62 | git push origin gh-pages 63 | else 64 | echo "No changes to commit" 65 | fi -------------------------------------------------------------------------------- /vulnerabilities/insufficient-access-control.md: -------------------------------------------------------------------------------- 1 | ## Insufficient Access Control 2 | 3 | Access control is often imperative in management and ownership of smart contracts. It's important to consider ways in which access control may be circumvented, or insufficiently implemented and the corresponding consequences. Contracts may allow any user to execute sensitive functions without verifying their authorization status. For example, a contract may have functions that transfer ownership, mint tokens, or modify critical state variables without checking if the caller has the appropriate permissions. 4 | 5 | Improper implementation of access control can lead to severe vulnerabilities, allowing unauthorized users to manipulate the contract’s state or even drain its funds. For example: 6 | 7 | ```solidity 8 | // UNSECURE 9 | function setInterestRate(uint256 _interestRate) public { 10 | // No access modifier, so anyone can change the interestRate 11 | interestRate = _interestRate; 12 | } 13 | ``` 14 | 15 | The `setInterestRate` function doesn’t have any access control which means anyone can call it to change the `interestRate` which can lead to severe consequences. We can resolve this by adding authorization logic, e.g.: 16 | 17 | ```solidity 18 | modifier onlyOwner() { 19 | require(msg.sender == owner, "Only the owner can call this function"); 20 | _; 21 | } 22 | 23 | function setInterestRate(uint256 _interestRate) public onlyOwner { 24 | interestRate = _interestRate; 25 | } 26 | ``` 27 | 28 | Now, the `onlyOwner` modifier has been added. This modifier checks if the caller of a function is the contract owner before allowing the function to proceed, ensuring that only the owner of the contract can change the `interestRate`. Similarly, role based access control mechanisms and whitelisting mechanisms can be implemented for proper access control. 29 | 30 | 31 | ### Sources 32 | - [Access Control Vulnerabilities in Smart Contracts](https://metaschool.so/articles/access-control-vulnerabilities-in-smart-contracts/) 33 | - [Mitigate Access Control Vulnerability](https://medium.com/rektify-ai/how-to-mitigate-access-control-vulnerability-6df74c82af98) 34 | 35 | -------------------------------------------------------------------------------- /vulnerabilities/unexpected-ecrecover-null-address.md: -------------------------------------------------------------------------------- 1 | ## Unexpected `ecrecover` Null Address 2 | 3 | `ecrecover` is a precompiled built-in cryptographic function which recovers an address associated with the public key from an elliptic curve signature or *returns zero on error*. The parameters corresponding to the signature are `r`, `s` & `v`. 4 | 5 | As noted above, `ecrecover` will return zero on error. It's possible to do this deterministically by setting `v` as any positive number other than 27 or 28. 6 | 7 | This can be manipulated by attackers to make it seem like a valid message has been signed by `address(0)`. 8 | 9 | > **NOTE:** The default value for addresses in solidity is `address(0)`. As such, in case important storage variables, e.g. owner/admin, are unset, it's possible to spoof a signature from one of these unset addresses, executing authorized-only logic. 10 | 11 | ```solidity 12 | // UNSECURE 13 | function validateSigner(address signer, bytes32 message, uint8 v, bytes32 r, bytes32 s) internal pure returns (bool) { 14 | address recoveredSigner = ecrecover(message, v, r, s); 15 | return signer == recoveredSigner; 16 | } 17 | ``` 18 | 19 | The above method is intended to only return true if a valid signature is provided. However, as we know, if we set `v` to any value other than 27 or 28, the `recoveredSigner` will be the null address and if the provided `signer` is `address(0)`, the function will unexpectedly return `true`. 20 | 21 | We can mitigate this issue by reverting if the `recoveredSigner` address is null, e.g.: 22 | 23 | ```solidity 24 | function validateSigner(address signer, bytes32 message, uint8 v, bytes32 r, bytes32 s) internal pure returns (bool) { 25 | address recoveredSigner = ecrecover(message, v, r, s); 26 | require(recoveredSigner != address(0)); 27 | return signer == recoveredSigner; 28 | } 29 | ``` 30 | 31 | ### Sources 32 | 33 | - [Solidity Documentation: Mathematical and Cryptographic Functions](https://docs.soliditylang.org/en/latest/units-and-global-variables.html#mathematical-and-cryptographic-functions) 34 | - [Ethereum Stack Exchange Answer](https://ethereum.stackexchange.com/a/69329) 35 | -------------------------------------------------------------------------------- /vulnerabilities/insufficient-gas-griefing.md: -------------------------------------------------------------------------------- 1 | ## Insufficient Gas Griefing 2 | 3 | Insufficient gas griefing can be done on contracts which accept data and use it in a sub-call on another contract. This method is often used in multisignature wallets as well as transaction relayers. If the sub-call fails, either the whole transaction is reverted, or execution is continued. 4 | 5 | Let's consider a simple relayer contract as an example. As shown below, the relayer contract allows someone to make and sign a transaction, without having to execute the transaction. Often this is used when a user can't pay for the gas associated with the transaction. 6 | 7 | ```solidity 8 | contract Relayer { 9 | mapping (bytes => bool) executed; 10 | 11 | function relay(bytes _data) public { 12 | // replay protection; do not call the same transaction twice 13 | require(executed[_data] == 0, "Duplicate call"); 14 | executed[_data] = true; 15 | innerContract.call(bytes4(keccak256("execute(bytes)")), _data); 16 | } 17 | } 18 | ``` 19 | 20 | The user who executes the transaction, the 'forwarder', can effectively censor transactions by using just enough gas so that the transaction executes, but not enough gas for the sub-call to succeed. 21 | 22 | There are two ways this could be prevented. The first solution would be to only allow trusted users to relay transactions. The other solution is to require that the forwarder provides enough gas, as seen below. 23 | 24 | ```solidity 25 | // contract called by Relayer 26 | contract Executor { 27 | function execute(bytes _data, uint _gasLimit) { 28 | require(gasleft() >= _gasLimit); 29 | ... 30 | } 31 | } 32 | ``` 33 | 34 | ### Sources 35 | 36 | - [SCSFG - Griefing](https://scsfg.io/hackers/griefing/) 37 | - [Ethereum Stack Exchange - What does griefing mean?](https://ethereum.stackexchange.com/questions/62829/what-does-griefing-mean) 38 | - [Ethereum Stack Exchange - Griefing attacks: Are they profitable for the attacker?](https://ethereum.stackexchange.com/questions/73261/griefing-attacks-are-they-profitable-for-the-attacker) 39 | - [Wikipedia - Griefer](https://en.wikipedia.org/wiki/Griefer) 40 | -------------------------------------------------------------------------------- /vulnerabilities/timestamp-dependence.md: -------------------------------------------------------------------------------- 1 | ## Timestamp Dependence 2 | 3 | **NOTE: This vulnerability no longer affects Ethereum mainnet as of the Proof of Stake merge. [Read more](https://ethereum.stackexchange.com/a/140818)** 4 | 5 | The timestamp of a block, accessed by `block.timestamp` or alias `now` can be manipulated by a miner. There are three considerations you should take into account when using a timestamp to execute a contract function. 6 | 7 | ### Timestamp Manipulation 8 | 9 | If a timestamp is used in an attempt to generate randomness, a miner can post a timestamp within 15 seconds of block validation, giving them the ability to set the timestamp as a value that would increase their odds of benefitting from the function. 10 | 11 | For example, a lottery application may use the block timestamp to pick a random bidder in a group. A miner may enter the lottery then modify the timestamp to a value that gives them better odds at winning the lottery. 12 | 13 | Timestamps should thus not be used to create randomness. See [Weak Sources of Randomness for Chain Attributes](weak-sources-randomness.md). 14 | 15 | ### The 15-second Rule 16 | 17 | Ethereum's reference specification, the Yellow Paper, doesn't specify a limit as to how much blocks can change in time, it just has to be bigger than the timestamp of its parent. This being said, popular protocol implementations reject blocks with timestamps greater than 15 seconds in the future, so as long as your time-dependent event can safely vary by 15 seconds, it may be safe to use a block timestamp. 18 | 19 | ### Don't use `block.number` as a timestamp 20 | 21 | You can estimate the time difference between events using `block.number` and the average block time, but block times may change and break the functionality, so it's best to avoid this use. 22 | 23 | 24 | ### Sources 25 | 26 | - [Consensys Smart Contract Best Practices - Timestamp Dependence (Attacks)](https://consensys.github.io/smart-contract-best-practices/attacks/timestamp-dependence/) 27 | - [Consensys Smart Contract Best Practices - Timestamp Dependence (Development Recommendations)](https://consensys.github.io/smart-contract-best-practices/development-recommendations/solidity-specific/timestamp-dependence/) 28 | -------------------------------------------------------------------------------- /vulnerabilities/authorization-txorigin.md: -------------------------------------------------------------------------------- 1 | ## Authorization Through tx.origin 2 | 3 | `tx.origin` is a global variable in Solidity which returns the address that sent a transaction. It's important that you never use `tx.origin` for authorization since another contract can use a fallback function to call your contract and gain authorization since the authorized address is stored in `tx.origin`. Consider this example: 4 | 5 | ```solidity 6 | 7 | // SPDX-License-Identifier: MIT 8 | 9 | pragma solidity ^0.8.0; 10 | 11 | // THIS CONTRACT CONTAINS A BUG - DO NOT USE 12 | contract TxUserWallet { 13 | address owner; 14 | 15 | constructor() public { 16 | owner = msg.sender; 17 | } 18 | 19 | function transferTo(address payable dest, uint amount) public { 20 | require(tx.origin == owner); 21 | dest.transfer(amount); 22 | } 23 | } 24 | ``` 25 | 26 | Here we can see that the `TxUserWallet` contract authorizes the `transferTo()` function with `tx.origin`. 27 | 28 | ```solidity 29 | 30 | // SPDX-License-Identifier: MIT 31 | 32 | pragma solidity ^0.8.0; 33 | 34 | interface TxUserWallet { 35 | function transferTo(address payable dest, uint amount) external; 36 | } 37 | 38 | contract TxAttackWallet { 39 | address payable private immutable owner; 40 | 41 | // Constructor sets the contract deployer as the owner 42 | constructor() { 43 | owner = payable(msg.sender); 44 | } 45 | 46 | // fallback function to receive Ether and trigger transfer 47 | 48 | fallback() external payable { 49 | // Call transferTo on TxUserWallet (msg.sender) to send its balance to owner 50 | 51 | TxUserWallet(msg.sender).transferTo(owner, msg.sender.balance); 52 | } 53 | } 54 | ``` 55 | 56 | Now if someone were to trick your 'TxUserWallet' contract into sending ether to the `TxAttackWallet` contract, they can steal all funds from 'TxUserWallet' by passing the `tx.origin` check. 57 | 58 | To prevent this kind of attack, use `msg.sender` for authorization. 59 | 60 | Examples from: https://solidity.readthedocs.io/en/develop/security-considerations.html#tx-origin 61 | 62 | ### Sources 63 | 64 | - [SWC-115](https://swcregistry.io/docs/SWC-115) 65 | - [Solidity Security Considerations - tx.origin](https://solidity.readthedocs.io/en/develop/security-considerations.html#tx-origin) 66 | - [Consensys Smart Contract Best Practices - tx.origin](https://consensys.github.io/smart-contract-best-practices/development-recommendations/solidity-specific/tx-origin/) 67 | - [SigP Solidity Security Blog - tx.origin](https://github.com/sigp/solidity-security-blog#tx-origin) 68 | -------------------------------------------------------------------------------- /vulnerabilities/unencrypted-private-data-on-chain.md: -------------------------------------------------------------------------------- 1 | ## Unencrypted Private Data On-Chain 2 | 3 | Ethereum smart contract code, storage, and any data transacted on-chain can always be read. Treat it as such. Even if your code is not verified on Etherscan, attackers can still decompile or check transactions to and from it to analyze it. For this reason, it's imperative that private data is never stored on-chain unencrypted. 4 | 5 | ### Example 6 | 7 | Let's consider a scenario where players participate in an Odd or Even game. Each player submits a number, and the winner is determined by the sum of both numbers being odd or even. Here is a vulnerable implementation: 8 | 9 | ```solidity 10 | // Vulnerable contract storing unencrypted private data 11 | contract OddEven { 12 | struct Player { 13 | address payable addr; 14 | uint number; 15 | } 16 | 17 | Player[2] private players; 18 | uint8 count = 0; 19 | 20 | function play(uint number) public payable { 21 | require(msg.value == 1 ether); 22 | players[count] = Player(payable(msg.sender), number); 23 | count++; 24 | if (count == 2) selectWinner(); 25 | } 26 | 27 | function selectWinner() private { 28 | uint n = players[0].number + players[1].number; 29 | players[n % 2].addr.transfer(address(this).balance); 30 | delete players; 31 | count = 0; 32 | } 33 | } 34 | ``` 35 | 36 | In this contract, the `players` array stores the submitted numbers in plain text. Although the `players` array is marked as private, this only means it is not accessible via other smart contracts. However, anyone can read the blockchain and view the stored values. This means the first player's number will be visible, allowing the second player to select a number that they know will make them a winner. 37 | 38 | ### Protection Mechanisms 39 | To protect sensitive data, consider the following strategies: 40 | 41 | 1) Commit-Reveal Scheme: Use a commit-reveal scheme where the data is committed to the blockchain in one phase and revealed in another. 42 | 2) Zero-Knowledge Proofs: Use cryptographic techniques such as zero-knowledge proofs to validate data without revealing it. 43 | 44 | ### References 45 | 46 | - [Medium Article: Attack Vectors in Solidity #4: Unencrypted Private Data On-Chain](https://medium.com/@natachigram/attack-vectors-in-solidity-4-unencrypted-private-data-on-chain-cf4f3ff1cf71) 47 | - [Solidity by Example: Accessing Private Data](https://solidity-by-example.org/hacks/accessing-private-data/) 48 | - [SWC Registry: Unencrypted Private Data On-Chain](https://swcregistry.io/docs/SWC-136) 49 | - [Zero-Knowledge Proofs](https://blog.ethereum.org/2016/12/05/zksnarks-in-a-nutshell/) -------------------------------------------------------------------------------- /vulnerabilities/msgvalue-loop.md: -------------------------------------------------------------------------------- 1 | ## Using ``msg.value`` in a Loop 2 | 3 | The value of ``msg.value`` in a transaction’s call never gets updated, even if the called contract ends up sending some or all of the ETH to another contract. This means that using ``msg.value`` in ``for`` or ``while`` loops, without extra accounting logic, will either lead to the transaction reverting (when there are no longer sufficient funds for later iterations), or to the contract being drained (when the contract itself has an ETH balance). 4 | 5 | ```solidity 6 | contract depositer { 7 | function deposit(address weth) payable external { 8 | for (uint i = 0; i < 5; i ++) { 9 | WETH(weth).deposit{value: msg.value}(); 10 | } 11 | } 12 | } 13 | ``` 14 | In the above example, first iteration will use all the ``msg.value`` for the external call and all other iterations can: 15 | - Drain the contract if enough ETH balance exists inside the contract to cover all the iterations. 16 | - Revert if enough ETH balance doesn't exist inside the contract to cover all the iterations. 17 | - Succeed if the external implementation succeeds with zero value transfers. 18 | 19 | Also, if a function has a check like ``require(msg.value == 1e18, "Not Enough Balance")``, that function can be called multiple times in a same transaction by sending ``1 ether`` once as ``msg.value`` is not updated in a transaction call. 20 | 21 | ```solidity 22 | function batchBuy(address[] memory addr) external payable{ 23 | mapping (uint => address) nft; 24 | 25 | for (uint i = 0; i < addr.length; i++) { 26 | buy1NFT(addr[i]) 27 | } 28 | 29 | function buy1NFT(address to) internal { 30 | if (msg.value < 1 ether) { // buy unlimited times after sending 1 ether once 31 | revert("Not enough ether"); 32 | } 33 | nft[numero] = address; 34 | } 35 | } 36 | ``` 37 | 38 | Thus, using ``msg.value`` inside a loop is dangerous because this might allow the sender to ``re-use`` the ``msg.value``. 39 | 40 | Reuse of ``msg.value`` can also show up with payable multicalls. Multicalls enable a user to submit a list of transactions to avoid paying the 21,000 gas transaction fee over and over. However, If ``msg.value`` gets ``re-used`` while looping through the functions to execute, it can cause a serious issue like the [Opyn Hack](https://peckshield.medium.com/opyn-hacks-root-cause-analysis-c65f3fe249db). 41 | 42 | ### Sources 43 | 44 | - [Rare Skills - Smart Contract Security](https://www.rareskills.io/post/smart-contract-security#:~:text=Using%20msg.,show%20up%20with%20payable%20multicalls.) 45 | - [TrustChain - Ethereum msg.value Reuse Vulnerability](https://trustchain.medium.com/ethereum-msg-value-reuse-vulnerability-5afd0aa2bcef) -------------------------------------------------------------------------------- /vulnerabilities/signature-malleability.md: -------------------------------------------------------------------------------- 1 | ## Signature Malleability 2 | 3 | It's generally assumed that a valid signature cannot be modified without the private key and remain valid. However, it is possible to modify a signature and maintain validity. One example of a system which is vulnerable to signature malleability is one in which validation as to whether an action can be executed is determined based on whether the signature has been previously used. 4 | 5 | ```solidity 6 | // UNSECURE 7 | require(!signatureUsed[signature]); 8 | 9 | // Validate signer and perform state modifying logic 10 | ... 11 | 12 | signatureUsed[signature] = true; 13 | ``` 14 | 15 | In the above example, we can see that the `signature` is saved in a `signatureUsed` mapping after execution and validated to not exist in that mapping before execution. The problem with this is that if the `signature` can be modified while maintaining validity, the transaction can be repeated by an attacker. 16 | 17 | ### How it works 18 | 19 | To understand how signature malleability works, we first need to understand a bit about elliptic curve cryptography. 20 | 21 | An elliptic curve consists of all the points that satisfy an equation of the form: 22 | 23 | $y^2 = x^3 + ax + b$ 24 | 25 | where 26 | 27 | $4a^3 + 27b^2 \not= 0$ (to avoid singular points) 28 | 29 | Some examples: 30 | 31 | ![Elliptic Curves](./img/elliptic-curves.png) 32 | 33 | Note that the curves are always symmetrical about the x-axis 34 | 35 | The curve used by Ethereum is secp256k1, which looks like this: 36 | 37 | ![secp256k1](./img/secp256k1.png) 38 | 39 | Now that we understand the basics of elliptic curve cryptography, we can dig into how signature malleability actually works on Ethereum. 40 | 41 | Ethereum uses [ECDSA](https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm) as its signature scheme. ECDSA signatures consist of a pair of numbers, $(r, s)$, with an integer order $n$. As a result of the x-axis symmetry, if $(r, s)$ is a valid signature, then so is $(r, -s$ mod $n)$. 42 | 43 | It's possible to calculate this complementary signature without knowing the private key used to produce it in the first place, which gives an attacker the ability to produce a second valid signature. 44 | 45 | ### Mitigation 46 | 47 | To avoid this issue, it's imperative to recognize that validating that a signature is not reused is insufficient in enforcing that the transaction is not replayed. 48 | 49 | 50 | ### Sources 51 | 52 | - [SWC-117](https://swcregistry.io/docs/SWC-117) 53 | - [Bitcoin Transaction Malleability](https://eklitzke.org/bitcoin-transaction-malleability) 54 | - [The Math Behind Elliptic Curve Cryptography](https://hackernoon.com/what-is-the-math-behind-elliptic-curve-cryptography-f61b25253da3) 55 | -------------------------------------------------------------------------------- /vulnerabilities/weak-sources-randomness.md: -------------------------------------------------------------------------------- 1 | ## Weak Sources of Randomness from Chain Attributes 2 | 3 | Using chain attributes for randomness, e.g.: `block.timestamp`, `blockhash`, and `block.difficulty` can seem like a good idea since they often produce pseudo-random values. The problem, however, is that Ethereum is entirely deterministic and all available on-chain data is public. Chain attributes can either be predicted or manipulated, and should thus never be used for random number generation. 4 | 5 | ### Example of Weak Randomness 6 | 7 | ```solidity 8 | pragma solidity ^0.8.24; 9 | 10 | contract GuessTheRandomNumber { 11 | constructor() payable {} 12 | 13 | function guess(uint256 _guess) public { 14 | uint256 answer = uint256( 15 | keccak256( 16 | abi.encodePacked(blockhash(block.number - 1), block.timestamp) 17 | ) 18 | ); 19 | 20 | if (_guess == answer) { 21 | (bool sent,) = msg.sender.call{value: 1 ether}(""); 22 | require(sent, "Failed to send Ether"); 23 | } 24 | } 25 | } 26 | ``` 27 | In the above example, the answer variable is initialized using `blockhash(block.number - 1)` and `block.timestamp`. This method is insecure because both `blockhash` and `block.timestamp` can be retrieved directly by another contract just in time, making it possible to guess the answer and win the challenge unfairly. 28 | 29 | An attacker can exploit the weak randomness as follows: 30 | 31 | ```solidity 32 | contract Attack { 33 | receive() external payable {} 34 | 35 | function attack(GuessTheRandomNumberChallenge guessTheRandomNumber) public { 36 | uint256 answer = uint256( 37 | keccak256( 38 | abi.encodePacked(blockhash(block.number - 1), block.timestamp) 39 | ) 40 | ); 41 | 42 | guessTheRandomNumber.guess(uint8(answer)); 43 | } 44 | 45 | // Helper function to check balance 46 | function getBalance() public view returns (uint256) { 47 | return address(this).balance; 48 | } 49 | } 50 | ``` 51 | The `Attack` contract calculates the `answer` using the same logic as the `GuessTheRandomNumber` contract and guesses it correctly, allowing the attacker to win the challenge. 52 | 53 | ### Preventive Measures 54 | 55 | A common and more secure solution is to use an oracle service such as [Chainlink VRF](https://docs.chain.link/vrf/v2/introduction/), which provides verifiable randomness that cannot be manipulated. 56 | 57 | ### Sources 58 | 59 | - [SWC Registry: SWC-120](https://swcregistry.io/docs/SWC-120) 60 | - [When can blockhash be safely used for a random number? When would it be unsafe?](https://ethereum.stackexchange.com/questions/419/when-can-blockhash-be-safely-used-for-a-random-number-when-would-it-be-unsafe) 61 | - [How can I securely generate a random number in my smart contract?](https://ethereum.stackexchange.com/questions/191/how-can-i-securely-generate-a-random-number-in-my-smart-contract) 62 | - [Solidity Patterns: Randomness](https://fravoll.github.io/solidity-patterns/randomness.html) -------------------------------------------------------------------------------- /vulnerabilities/unsafe-low-level-call.md: -------------------------------------------------------------------------------- 1 | ## Unsafe Low-Level Call 2 | 3 | In Solidity, you can either use low-level calls such as: `address.call()`, `address.callcode()`, `address.delegatecall()`, and `address.send()`; or you can use contract calls such as: `ExternalContract.doSomething()`. 4 | 5 | Low-level calls can be a good way to efficiently or arbitrarily make contract calls. However, it's important to be aware of the caveats it possesses. 6 | 7 | ### Unchecked call return value 8 | 9 | Low-level calls will never throw an exception, instead they will return `false` if they encounter an exception, whereas contract calls will automatically throw. Thus if the return value of a low-level call is not checked, the execution may resume even if the function call throws an error. This can lead to unexpected behaviour and break the program logic. A failed call can even be caused intentionally by an attacker, who may be able to further exploit the application. 10 | 11 | In the case that you use low-level calls, be sure to check the return value to handle possible failed calls, e.g.: 12 | 13 | ```solidity 14 | // Simple transfer of 1 ether 15 | (bool success,) = to.call{value: 1 ether}(""); 16 | // Revert if unsuccessful 17 | require(success); 18 | ``` 19 | 20 | ### Successful call to non-existent contract 21 | 22 | As noted in the [Solidity docs](https://docs.soliditylang.org/en/v0.8.15/control-structures.html?highlight=low%20level%20calls#external-function-calls): "Due to the fact that the EVM considers a call to a non-existing contract to always succeed, Solidity uses the `extcodesize` opcode to check that the contract that is about to be called actually exists (it contains code) and causes an exception if it does not. This check is skipped if the return data will be decoded after the call and thus the ABI decoder will catch the case of a non-existing contract. 23 | 24 | Note that this check is not performed in case of [low-level calls](https://docs.soliditylang.org/en/v0.8.15/units-and-global-variables.html#address-related) which operate on addresses rather than contract instances." 25 | 26 | It's imperative that we do not simply assume that a contract to be called via a low-level call actually exists, since if it doesn't our logic will proceed even though our external call effectively failed. This can lead to loss of funds and/or an invalid contract state. Instead, we must verify that the contract being called exists, either immediately before being called with an `extcodesize` check, or by verifying during contract deployment and using a `constant`/`immutable` value if the contract can be fully trusted. 27 | 28 | ```solidity 29 | // Verify address is a contract 30 | require(to.code.length > 0); 31 | // Simple transfer of 1 ether 32 | (bool success,) = to.call{value: 1 ether}(""); 33 | // Revert if unsuccessful 34 | require(success); 35 | ``` 36 | 37 | 38 | ### Sources 39 | 40 | - [SWC-104: Record Replay](https://swcregistry.io/docs/SWC-104) 41 | - [Consensys Smart Contract Best Practices - External Calls](https://consensys.github.io/smart-contract-best-practices/development-recommendations/general/external-calls/) 42 | -------------------------------------------------------------------------------- /vulnerabilities/delegatecall-untrusted-callee.md: -------------------------------------------------------------------------------- 1 | ## Delegatecall to Untrusted Callee 2 | 3 | `Delegatecall` is a special variant of a message call. It is almost identical to a regular message call except the target address is executed in the context of the calling contract and `msg.sender` and `msg.value` remain the same. Essentially, `delegatecall` delegates other contracts to modify the calling contract's storage. 4 | 5 | Since `delegatecall` gives so much control over a contract, it's very important to only use this with trusted contracts such as your own. If the target address comes from user input, be sure to verify that it is a trusted contract. 6 | 7 | ### Example 8 | 9 | Consider the following contracts where `delegatecall` is misused, leading to a vulnerability: 10 | 11 | ```solidity 12 | // SPDX-License-Identifier: MIT 13 | pragma solidity ^0.8.16; 14 | 15 | contract Proxy { 16 | address public owner; 17 | 18 | constructor() { 19 | owner = msg.sender; 20 | } 21 | 22 | function forward(address callee, bytes calldata _data) public { 23 | require(callee.delegatecall(_data), "Delegatecall failed"); 24 | } 25 | } 26 | 27 | contract Target { 28 | address public owner; 29 | 30 | function pwn() public { 31 | owner = msg.sender; 32 | } 33 | } 34 | 35 | contract Attack { 36 | address public proxy; 37 | 38 | constructor(address _proxy) { 39 | proxy = _proxy; 40 | } 41 | 42 | function attack(address target) public { 43 | Proxy(proxy).forward(target, abi.encodeWithSignature("pwn()")); 44 | } 45 | } 46 | ``` 47 | 48 | In this example, the `Proxy` contract uses `delegatecall` to forward any call it receives to an address provided by the user. The `Target` contract contains a call to the `pwn()` function that changes the owner of the contract to the caller. 49 | 50 | The `Attack` contract takes advantage of this setup by calling the `forward` function of the `Proxy` contract, passing the address of the `Target` contract and the encoded function call `pwn()`. This results in the `Proxy` contract's storage being modified, specifically the `owner` variable, which is set to the attacker’s address. 51 | 52 | ### Mitigations 53 | 54 | To mitigate the risks associated with `delegatecall` to untrusted callees, consider the following strategies: 55 | 56 | 1. **Whitelist Trusted Contracts**: Ensure that the target address for `delegatecall` is a contract you control or a contract that is part of a verified and trusted list. 57 | 58 | 2. **Limit the Scope of Delegatecall**: Use `delegatecall` only for specific, controlled operations. Avoid exposing it as a general-purpose function unless absolutely necessary. 59 | 60 | ### Sources 61 | 62 | - [SWC Registry: SWC-112](https://swcregistry.io/docs/SWC-112) 63 | - [Solidity Documentation: Delegatecall](https://docs.soliditylang.org/en/latest/introduction-to-smart-contracts.html#delegatecall-and-libraries) 64 | - [Sigma Prime: Solidity Security](https://blog.sigmaprime.io/solidity-security.html#delegatecall) 65 | - [Ethereum Stack Exchange: Difference Between Call, Callcode, and Delegatecall](https://ethereum.stackexchange.com/questions/3667/difference-between-call-callcode-and-delegatecall) 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Smart Contract Vulnerabilities 2 | 3 | A collection of smart contract vulnerabilities along with prevention methods 4 | 5 | ### Access Control 6 | 7 | - [Authorization Through tx.origin](./vulnerabilities/authorization-txorigin.md) 8 | - [Insufficient Access Control](./vulnerabilities/insufficient-access-control.md) 9 | - [Delegatecall to Untrusted Callee](./vulnerabilities/delegatecall-untrusted-callee.md) 10 | - [Signature Malleability](./vulnerabilities/signature-malleability.md) 11 | - [Missing Protection against Signature Replay Attacks](./vulnerabilities/missing-protection-signature-replay.md) 12 | 13 | ### Math 14 | 15 | - [Integer Overflow and Underflow](./vulnerabilities/overflow-underflow.md) 16 | - [Off-By-One](./vulnerabilities/off-by-one.md) 17 | - [Lack of Precision](./vulnerabilities/lack-of-precision.md) 18 | 19 | ### Control Flow 20 | 21 | - [Reentrancy](./vulnerabilities/reentrancy.md) 22 | - [DoS with Block Gas Limit](./vulnerabilities/dos-gas-limit.md) 23 | - [DoS with (Unexpected) revert](./vulnerabilities/dos-revert.md) 24 | - [Using `msg.value` in a Loop](./vulnerabilities/msgvalue-loop.md) 25 | - [Transaction-Ordering Dependence](./vulnerabilities/transaction-ordering-dependence.md) 26 | - [Insufficient Gas Griefing](./vulnerabilities/insufficient-gas-griefing.md) 27 | 28 | ### Data Handling 29 | 30 | - [Unchecked Return Value](./vulnerabilities/unchecked-return-values.md) 31 | - [Write to Arbitrary Storage Location](./vulnerabilities/arbitrary-storage-location.md) 32 | - [Unbounded Return Data](./vulnerabilities/unbounded-return-data.md) 33 | - [Uninitialized Storage Pointer](./vulnerabilities/uninitialized-storage-pointer.md) 34 | - [Unexpected `ecrecover` null address](./vulnerabilities/unexpected-ecrecover-null-address.md) 35 | 36 | ### Unsafe Logic 37 | 38 | - [Weak Sources of Randomness from Chain Attributes](./vulnerabilities/weak-sources-randomness.md) 39 | - [Hash Collision when using abi.encodePacked() with Multiple Variable-Length Arguments](./vulnerabilities/hash-collision.md) 40 | - [Timestamp Dependence](./vulnerabilities/timestamp-dependence.md) 41 | - [Unsafe Low-Level Call](./vulnerabilities/unsafe-low-level-call.md) 42 | - [Unsupported Opcodes](./vulnerabilities/unsupported-opcodes.md) 43 | - [Unencrypted Private Data On-Chain](./vulnerabilities/unencrypted-private-data-on-chain.md) 44 | - [Asserting Contract from Code Size](./vulnerabilities/asserting-contract-from-code-size.md) 45 | 46 | ### Code Quality 47 | 48 | - [Floating Pragma](./vulnerabilities/floating-pragma.md) 49 | - [Outdated Compiler Version](./vulnerabilities/outdated-compiler-version.md) 50 | - [Use of Deprecated Functions](./vulnerabilities/use-of-deprecated-functions.md) 51 | - [Incorrect Constructor Name](./vulnerabilities/incorrect-constructor.md) 52 | - [Shadowing State Variables](./vulnerabilities/shadowing-state-variables.md) 53 | - [Incorrect Inheritance Order](./vulnerabilities/incorrect-inheritance-order.md) 54 | - [Presence of Unused Variables](./vulnerabilities/unused-variables.md) 55 | - [Default Visibility](./vulnerabilities/default-visibility.md) 56 | - [Inadherence to Standards](./vulnerabilities/inadherence-to-standards.md) 57 | - [Assert Violation](./vulnerabilities/assert-violation.md) 58 | - [Requirement Violation](./vulnerabilities/requirement-violation.md) 59 | -------------------------------------------------------------------------------- /vulnerabilities/unsupported-opcodes.md: -------------------------------------------------------------------------------- 1 | ## Unsupported Opcodes 2 | 3 | EVM-compatible chains, such as zkSync Era, BNB Chain, Polygon, Optimism and Arbitrum implement the Ethereum Virtual Machine (EVM) and its opcodes. However, opcode support can vary across these chains, which can lead to bugs and issues if not considered during smart contract development and deployment. 4 | 5 | ### PUSH0 Opcode Compatibility 6 | The `PUSH0` opcode was introduced during the Shanghai hard fork of the Shapella upgrade (Solidity v0.8.20) and is available in certain EVM-compatible chains. However, not all chains have implemented support for this opcode yet. 7 | 8 | #### Is `PUSH0` supported: 9 | 1. Ethereum: YES 10 | 2. Arbitrum One: YES 11 | 3. Optimism: YES 12 | 4. ... 13 | 14 | More chain differences and opcode support can be found on: [evmdiff.com](https://www.evmdiff.com) 15 | 16 | You can also check compatibility by running the following command assuming you have Foundry set up: 17 | 18 | ```bash 19 | cast call --rpc-url $ARBITRUM_RPC_URL --create 0x5f 20 | ``` 21 | 22 | Getting a `0x` response from running the above command means the opcode is supported; an error indicates the opcode isn't supported on that chain. 23 | 24 | ### CREATE and CREATE2 on zkSync Era 25 | On zkSync Era, contract deployment uses the hash of the bytecode and the `factoryDeps` field of EIP712 transactions contains the bytecode. The actual deployment occurs by providing the contract's hash to the `ContractDeployer` system contract. 26 | 27 | To ensure that `create` and `create2` functions operate correctly, the compiler MUST be aware of the bytecode of the deployed contract in advance. The compiler interprets the calldata arguments as incomplete input for `ContractDeployer`, with the remaining part filled in by the compiler internally. The Yul `datasize` and `dataoffset` instructions have been adjusted to return the constant size and bytecode hash rather than the bytecode itself. 28 | 29 | The following code will not function correctly because the compiler is not aware of the bytecode beforehand but will work fine on Ethereum Mainnet: 30 | 31 | ```solidity 32 | function myFactory(bytes memory bytecode) public { 33 | assembly { 34 | addr := create(0, add(bytecode, 0x20), mload(bytecode)) 35 | } 36 | } 37 | ``` 38 | 39 | ### `.transfer()` on zkSync Era 40 | The `transfer()` function in Solidity is limited to 2300 gas, which can be insufficient if the receiving contract's fallback or receive function involves more complex logic. This can lead to the transaction reverting if the gas limit is exceeded. 41 | 42 | It is for this exact reason that the Gemholic project on zkSync Era locked its 921 ETH that was raised during the Gemholic token sale making the funds inaccessible. 43 | 44 | This was because the contract deployment did not account for zkSync Era's handling of the `.transfer()` function. 45 | 46 | ### Sources 47 | 48 | - [zkSync Era docs](https://docs.zksync.io/build/developer-reference/differences-with-ethereum.html#create-create2) 49 | - [CodeHawks first flight submission: The TokenFactory.sol can't deploy on the ZKSync Era](https://www.codehawks.com/submissions/clomptuvr0001ie09bzfp4nqw/4) 50 | - [GemstoneIDO Contract Issue Analysis on Medium](https://medium.com/coinmonks/gemstoneido-contract-stuck-with-921-eth-an-analysis-of-why-transfer-does-not-work-on-zksync-era-d5a01807227d) 51 | -------------------------------------------------------------------------------- /style-guide.md: -------------------------------------------------------------------------------- 1 | # Smart Contract Vulnerability Style Guide 2 | This style guide outlines the formatting and content expectations for contributions to this repository, focusing on vulnerabilities in smart contracts deployed on Ethereum Virtual Machine [(EVM)-compatible](https://ethereum.org/en/developers/docs/evm/) chains. 3 | 4 | ## Document Structure 5 | - *Markdown (.md) Files:* Content will primarily be authored in [markdown format](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax) for improved readability and version control. 6 | - *Consistent Naming:* Use descriptive file names that convey the vulnerability discussed. Examples: `unsupported-opcodes.md`, `default-visibility.md`. 7 | - *Heading Hierarchy:* Utilize clear heading levels (##, ###, etc.) to structure content and improve navigation. 8 | 9 | ## Content Guidelines 10 | - *Vulnerability Type:* Identify the type of vulnerability at the beginning of the document (eg. Unsupported Opcodes, Reentrancy, Access Control). 11 | - *Technical Explanation:* Provide a concise technical explanation of the vulnerability, including potential impact and exploit scenarios. Use code snippets where necessary to illustrate the issue. 12 | - *Affected Chains (Optional):* Specify which EVM-compatible chains are susceptible to the vulnerability. Highlight any chain-specific considerations. 13 | - *Detection and Mitigation (Optional):* Outline recommended methods for detecting the vulnerability during smart contract audits and suggest mitigation strategies for developers. Tools and best practices can be included here. 14 | - *Examples (Optional):* If applicable, include real-world examples of smart contracts impacted by the vulnerability. 15 | - *Severity Rating (Optional):* Consider incorporating a severity rating system to prioritize vulnerabilities based on potential impact. 16 | - *Updating the README:* When you add a new vulnerability and its corresponding markdown file, remember to update `README.md` with the new entry. 17 | 18 | ## Code Snippet Formatting 19 | - *Code Blocks:* Use [markdown code blocks](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks) to present code snippets. 20 | - *Syntax Highlighting:* Enable [syntax highlighting](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks#syntax-highlighting) for Solidity code using appriopriate markdown extensions or tools to enhance readability. 21 | - *Comments:* Include comments within code snippets where necessary to explain specific lines or logic. 22 | 23 | ## External References 24 | - *Links:* Link to relevant resources such as official chain documentation, vulnerability reports, and blog posts for further exploration. 25 | - *Citations:* Use clear in-text citations and a dedicated "Sources" section to reference external sources. 26 | 27 | ## Additional Considerations 28 | - *Target Audience:* Tailor the level of technical detail to a broad audience with an interest in smart contract security. 29 | - *Concise and Actionable:* Focus on providing actionable information to help developers identify and prevent vulnerabilities. 30 | - *Community Contributions:* Encourage community contributions and maintain a welcoming environment for pull requests and discussions. 31 | - *Versioning:* Maintain a clear versioning system to track updates and changes to vulnerabilities. -------------------------------------------------------------------------------- /vulnerabilities/dos-gas-limit.md: -------------------------------------------------------------------------------- 1 | ## DoS with Block Gas Limit 2 | 3 | One of the primary benefits of a block gas limit is that it prevents attackers from creating an infinite transaction loop. If the gas usage of a transaction exceeds this limit, the transaction will fail. However, along with this benefit comes a side effect which is important to understand. 4 | 5 | ### Unbounded Operations 6 | 7 | An example in which the block gas limit can be an issue is in executing logic in an unbounded loop. Even without any malicious intent, this can easily go wrong. Just by e.g., having too large an array of users to send funds to can exceed the gas limit and prevent the transaction from ever succeeding, potentially permanently locking up funds. 8 | 9 | This situation can also lead to an attack. Say a bad actor decides to create a significant amount of addresses, with each address being paid a small amount of funds from the smart contract. If done effectively, the transaction can be blocked indefinitely, possibly even preventing further transactions from going through. 10 | 11 | An effective solution to this problem would be to use a pull payment system over the above push payment system. To do this, separate each payment into its own transaction, and have the recipient call the function. 12 | 13 | If, for some reason, you really need to loop through an array of unspecified length, at least expect it to potentially take multiple blocks, and allow it to be performed in multiple transactions - as seen in this example: 14 | 15 | ```solidity 16 | struct Payee { 17 | address addr; 18 | uint256 value; 19 | } 20 | 21 | Payee[] payees; 22 | uint256 nextPayeeIndex; 23 | 24 | function payOut() { 25 | uint256 i = nextPayeeIndex; 26 | while (i < payees.length && msg.gas > 200000) { 27 | payees[i].addr.send(payees[i].value); 28 | i++; 29 | } 30 | nextPayeeIndex = i; 31 | } 32 | ``` 33 | 34 | ### Block Stuffing 35 | 36 | In some situations, your contract can be attacked with a block gas limit even if you don't loop through an array of unspecified length. An attacker can fill several blocks before a transaction can be processed by using a sufficiently high gas price. 37 | 38 | This attack is done by issuing several transactions at a very high gas price. If the gas price is high enough, and the transactions consume enough gas, they can fill entire blocks and prevent other transactions from being processed. 39 | 40 | Ethereum transactions require the sender to pay gas to disincentivize spam attacks, but in some situations, there can be enough incentive to go through with such an attack. For example, a block stuffing attack was used on a gambling Dapp, Fomo3D. The app had a countdown timer, and users could win a jackpot by being the last to purchase a key, except every time a user bought a key, the timer would be extended. An attacker bought a key then stuffed the next 13 blocks in a row so they could win the jackpot. 41 | 42 | To prevent such attacks from occurring, it's important to carefully consider whether it's safe to incorporate time-based actions in your application. 43 | 44 | Example from: [https://consensys.github.io/smart-contract-best-practices/attacks/denial-of-service/](https://consensys.github.io/smart-contract-best-practices/attacks/denial-of-service/) 45 | 46 | ### Sources 47 | 48 | - [Consensys Smart Contract Best Practices - Denial of Service](https://consensys.github.io/smart-contract-best-practices/attacks/denial-of-service/) 49 | - [Ethereum Developers Documentation - Gas](https://ethereum.org/en/developers/docs/gas/) 50 | -------------------------------------------------------------------------------- /vulnerabilities/unchecked-return-values.md: -------------------------------------------------------------------------------- 1 | ## Unchecked Return Values 2 | 3 | The main idea behind this type of vulnerability is the failure to properly handle the return values of external function calls. This can have significant consequences, including fund loss and unexpected behavior in the contract's logic. 4 | 5 | In Solidity, developers can perform external calls using methods like: 6 | 7 | 1. `.send()` 8 | 2. `.call()` 9 | 3. `.transfer()` 10 | 11 | `.transfer()` is commonly used to send ether to external accounts, however, the `.send()` function can also be used. For more versatile external calls, `.call()` can be used. 12 | 13 | Each of these methods has a different behavior when it comes to error handling. The `.call()` and `.send()` functions return a boolean indicating if the call succeeded or failed. Thus, these functions have a simple caveat: the transaction that executes these functions (`.call()` and `.send()`) WILL NOT revert if the external call fails. Instead, `.call()` and `.send()` will simply return the boolean value `false`. 14 | 15 | A common pitfall arises when the return value is not checked, as the developer expects a revert to occur when, in reality, the revert will not occur if not explicitly checked by the smart contract. 16 | 17 | For example, if a contract uses `.send()` without checking its return value, transaction execution will continue even if the call fails, resulting in unexpected behavior. Take the below contract for example: 18 | 19 | ```solidity 20 | /// INSECURE 21 | contract Lotto { 22 | 23 | bool public paidOut = false; 24 | address public winner; 25 | uint256 public winAmount; 26 | 27 | /// extra functionality here 28 | 29 | function sendToWinner() public { 30 | require(!paidOut); 31 | winner.send(winAmount); 32 | paidOut = true; 33 | } 34 | 35 | function withdrawLeftOver() public { 36 | require(paidOut); // requires `paidOut` to be true 37 | msg.sender.send(this.balance); 38 | } 39 | } 40 | ``` 41 | 42 | The above contract represents a Lotto-like contract, where a winner receives `winAmount` of ether, which typically leaves a little left over for anyone to withdraw. 43 | 44 | The bug exists where `.send()` is used without checking the response, i.e., `winner.send(winAmount)`. 45 | 46 | In this example, a winner whose transaction fails (either by running out of gas or being a contract that intentionally throws in the fallback function) will still allow `paidOut` to be set to true (regardless of whether ether was sent or not). 47 | 48 | In this case, anyone can withdraw the winner's winnings using the `withdrawLeftOver()` function. 49 | 50 | A more serious version of this bug occurred in [King of the Ether](https://www.kingoftheether.com/thrones/kingoftheether/index.html). An excellent [post-mortem](https://www.kingoftheether.com/postmortem.html) of this contract has been written, detailing how an unchecked failed `.send()` could be used to attack a contract. 51 | 52 | ### Preventive Techniques 53 | 54 | To mitigate this vulnerability, developers should always check the return value of any call to an external contract. The `require()` function can be used to check if the call was successful and handle any errors that may occur. 55 | 56 | A caveat developers should be wary of when using the `require()` function is unexpected reverts that can cause DoS. If the developer naively decides to check for the success or failure of the external `.send()` call like so: 57 | 58 | ```solidity 59 | /// INSECURE 60 | contract Lotto { 61 | 62 | bool public paidOut = false; 63 | address public winner; 64 | uint256 public winAmount; 65 | 66 | /// extra functionality here 67 | 68 | function sendToWinner() public { 69 | require(!paidOut); 70 | require(winner.send(winAmount)); // naively check success of the external call 71 | paidOut = true; 72 | } 73 | 74 | function withdrawLeftOver() public { 75 | require(paidOut); // requires `paidOut` to be true 76 | msg.sender.send(this.balance); 77 | } 78 | ``` 79 | 80 | An attacker interacting with the `Lotto` contract from their own malicious contract and calling the `sendToWinner` function, can just implement a fallback function that reverts all payments making `paidOut` not set to true! 81 | 82 | A detailed explanation of this caveat can be found [here](./dos-revert.md) 83 | 84 | ### Sources 85 | 86 | - [SigmaPrime blog post](https://blog.sigmaprime.io/solidity-security.html#unchecked-calls) 87 | - [DoS with an unexpected revert](./dos-revert.md) 88 | 89 | -------------------------------------------------------------------------------- /vulnerabilities/overflow-underflow.md: -------------------------------------------------------------------------------- 1 | ## Integer Overflow and Underflow 2 | In solidity, Integer types have maximum and minimum values. Integer overflow occurs when an integer variable exceeds the maximum value that can be stored in that variable type. Similarly, Integer underflow occurs when an integer variable goes below the minimum value for that variable type. Example: The maximum value ``uint8`` can store is ``255``. Now, when you store ``256`` in ``uint8`` it will overflow and the value will reset to 0. When you store ``257``, the value will be ``1``, ``2`` for ``258`` and so on. Similarly, if you try to store ``-1`` in the uint8 variable the value of the variable will become ``255``, and so on as it will underflow. 3 | 4 | Some integer types and their min/max values: 5 | | Type | Max | Min | 6 | |----------|-------------|------| 7 | | uint8 | 255 | 0 | 8 | | uint16 | 65535 | 0 | 9 | | uint24 | 16777215 | 0 | 10 | | uint256 | 2^256 - 1 | 0 | 11 | 12 | Since smaller integer types like: ``uint8``, ``uint16``, etc have smaller maximum values, it can be easier to cause an overflow, thus they should be used with greater caution. 13 | 14 | To prevent over/underflows, [Safe Math Library](https://github.com/ConsenSysMesh/openzeppelin-solidity/blob/master/contracts/math/SafeMath.sol) is often used by contracts with older versions of Solidity but Solidity >=0.8 protects against integer overflows and underflows through the use of built-in safe math functions. It's important to consider that regardless of SafeMath logic being used, either built-in or used manually in older contracts, over/underflows still trigger reverts, which may result in [denial of service](https://github.com/kadenzipfel/smart-contract-vulnerabilities/blob/master/vulnerabilities/dos-revert.md) of important functionality or other unexpected effects. Even after the update of solidity to 0.8, there are scenarios in which the integer overflow and underflow can still occur without the transaction reverting. 15 | 16 | ### Typecasting 17 | The most common way in which integer over/underflow is possible when you convert an integer of a larger uint data type to a smaller data type. 18 | ```solidity 19 | uint256 public a = 258; 20 | uint8 public b = uint8(a); // typecasting uint256 to uint8 21 | ``` 22 | The above code snippet will overflow and the ``2`` will be stored in the variable ``b`` due to the fact that maximum value in uint8 data type is ``255``. So, it will overflow and reset to ``0`` without reverting. 23 | 24 | ### Using Shift Operators 25 | Overflow & underflow checks are not performed for shift operations like they are performed for other arithmetic operations. Thus, over/underflows can occur. 26 | 27 | The left shift ``<<`` operator shifts all the beats in the first operand by the number specified in the second operand. Shifting an operand by 1 position is equivalent to multiplying it by 2, shifting 2 positions is equivalent to multiplying it by 4, and shifting 3 positions is equivalent to multiplying by 8. 28 | 29 | ```solidity 30 | uint8 public a = 100; 31 | uint8 public b = 2; 32 | 33 | uint8 public c = a << b; // overflow as 100 * 4 > 255 34 | ``` 35 | In the above code, left shifting ``a`` which is ``100`` by 2 positions ``b`` is equivalent to multiplying 100 by 4. So the result will overflow and the value in c will be ``144`` because ``400-256`` is ``144``. 36 | 37 | ### Use of Inline Assembly: 38 | Inline Assembly in solidity is performed using YUL language. In YUL programming language, integer underflow & overflow is possible as compiler does not check automatically for it as YUL is a low-level language that is mostly used for making the code more optimized, which does this by omitting many opcodes. 39 | 40 | ```solidity 41 | uint8 public a = 255; 42 | 43 | function addition() public returns (uint8 result) { 44 | assembly { 45 | result := add(sload(a.slot), 1) // adding 1 will overflow and reset to 0 46 | // using inline assembly 47 | } 48 | 49 | return result; 50 | } 51 | ``` 52 | In the above code we are adding ``1`` into the variable with inline assembly and then returning the result. The variable result will overflow and 0 will be returned, despite this the contract will not throw an error or revert. 53 | 54 | ### Use of unchecked code block: 55 | Performing arithmetic operations inside the unchecked block saves a lot of gas because it omits several checks and opcodes. But, some of these opcodes are used in default arithmetic operations in 0.8 to check for underflow/overflow. 56 | 57 | ```solidity 58 | uint8 public a = 255; 59 | 60 | function uncheck() public{ 61 | 62 | unchecked { 63 | a++; // overflow and reset to 0 without reverting 64 | } 65 | 66 | } 67 | ``` 68 | The unchecked code block is only recommended if you are sure that there is no possible way for the arithmetic to overflow or underflow. 69 | 70 | ### Sources 71 | 72 | - [Solidity Documentation - 0.8.0 Breaking Changes](https://docs.soliditylang.org/en/latest/080-breaking-changes.html) 73 | - [Medium - How Solidity 0.8 Protects Against Integer Underflow/Overflow and How They Can Still Happen](https://faizannehal.medium.com/how-solidity-0-8-protect-against-integer-underflow-overflow-and-how-they-can-still-happen-7be22c4ab92f) 74 | -------------------------------------------------------------------------------- /vulnerabilities/dos-revert.md: -------------------------------------------------------------------------------- 1 | ## DoS with (Unexpected) revert 2 | 3 | A DoS (Denial of Service) may be caused when logic is unable to be executed as a result of an unexpected revert. This can happen for a number of reasons and it's important to consider all the ways in which your logic may revert. The examples listed below are *non-exhaustive*. 4 | 5 | ### Reverting funds transfer 6 | 7 | DoS (Denial of Service) attacks can occur in functions when you try to send funds to a user and the functionality relies on that fund transfer being successful. 8 | 9 | This can be problematic in the case that the funds are sent to a smart contract created by a bad actor, since they can simply create a fallback function that reverts all payments. 10 | 11 | For example: 12 | 13 | ```solidity 14 | // INSECURE 15 | contract Auction { 16 | address currentLeader; 17 | uint highestBid; 18 | 19 | function bid() payable { 20 | require(msg.value > highestBid); 21 | 22 | require(currentLeader.send(highestBid)); // Refund the old leader, if it fails then revert 23 | 24 | currentLeader = msg.sender; 25 | highestBid = msg.value; 26 | } 27 | } 28 | ``` 29 | 30 | As you can see in this example, if an attacker bids from a smart contract with a fallback function reverting all payments, they can never be refunded, and thus no one can ever make a higher bid. 31 | 32 | This can also be problematic without an attacker present. For example, you may want to pay an array of users by iterating through the array, and of course you would want to make sure each user is properly paid. The problem here is that if one payment fails, the function is reverted and no one is paid. 33 | 34 | ```solidity 35 | address[] private refundAddresses; 36 | mapping (address => uint) public refunds; 37 | 38 | // bad 39 | function refundAll() public { 40 | for(uint x; x < refundAddresses.length; x++) { // arbitrary length iteration based on how many addresses participated 41 | require(refundAddresses[x].send(refunds[refundAddresses[x]])) // doubly bad, now a single failure on send will hold up all funds 42 | } 43 | } 44 | ``` 45 | 46 | An effective solution to this problem would be to use a pull payment system over the above push payment system. To do this, separate each payment into its own transaction, and have the recipient call the function. 47 | 48 | ```solidity 49 | contract auction { 50 | address highestBidder; 51 | uint highestBid; 52 | mapping(address => uint) refunds; 53 | 54 | function bid() payable external { 55 | require(msg.value >= highestBid); 56 | 57 | if (highestBidder != address(0)) { 58 | refunds[highestBidder] += highestBid; // record the refund that this user can claim 59 | } 60 | 61 | highestBidder = msg.sender; 62 | highestBid = msg.value; 63 | } 64 | 65 | function withdrawRefund() external { 66 | uint refund = refunds[msg.sender]; 67 | refunds[msg.sender] = 0; 68 | (bool success, ) = msg.sender.call.value(refund)(""); 69 | require(success); 70 | } 71 | } 72 | ``` 73 | 74 | 75 | Examples from: https://consensys.github.io/smart-contract-best-practices/attacks/denial-of-service/ 76 | https://consensys.github.io/smart-contract-best-practices/development-recommendations/general/external-calls/ 77 | 78 | ### Over/Underflow 79 | 80 | Prior to SafeMath usage, whether built-in in solidity >=0.8.0 or using a library, [over/underflows](./overflow-underflow.md) would result in rolling over to the minimum/maximum value. Now that checked math is commonplace, it's important to recognize that the effect of checked under/overflows is a revert, which may DoS important logic. 81 | 82 | Regardless of usage of checked math, it's necessary to ensure that any valid input will not result in an over/underflow. Take extra care when working with smaller integers e.g. `int8`/`uint8`, `int16`/`uint16`, `int24`/`uint24`, etc.. 83 | 84 | ### Unexpected Balance 85 | 86 | It's important to take caution in enforcing expected contract balances of tokens or Ether as those balances may be increased by an attacker to cause an unexpected revert. This is easily possible with ERC20 tokens by simply `transfer`ring to the contract, but is also possible with Ether by forcibly sending Ether to a contract. 87 | 88 | Consider, for example, a contract which expects the Ether balance to be 0 for the first deposit to allow for custom accounting logic. An attacker may forcibly send Ether to the contract before the first deposit, causing all deposits to revert. 89 | 90 | ### Divide by Zero 91 | In solidity if the contract attempts to perform division when the denominator is ``zero``, the call reverts. Thus, the denominator should be always checked before division to prevent DoS revert. 92 | ```solidity 93 | function foo(uint num, uint den) public pure returns(uint result) { 94 | result = num / den; // if den = 0, the execution reverts 95 | } 96 | ``` 97 | 98 | ### Sources 99 | 100 | - [Consensys Smart Contract Best Practices - Denial of Service](https://consensys.github.io/smart-contract-best-practices/attacks/denial-of-service/) 101 | - [Consensys Smart Contract Best Practices - External Calls](https://consensys.github.io/smart-contract-best-practices/development-recommendations/general/external-calls/) 102 | -------------------------------------------------------------------------------- /vulnerabilities/unbounded-return-data.md: -------------------------------------------------------------------------------- 1 | ## Unbounded Return Data 2 | 3 | The [Byzantium](https://blog.ethereum.org/2017/10/12/byzantium-hf-announcement) 2017 mainnet hard-fork introduced [EIP-211](https://eips.ethereum.org/EIPS/eip-211). This EIP established an arbitrary-length return data buffer as well as 2 new opcodes: `RETURNDATASIZE` and `RETURNDATACOPY`. This enables callers to copy all or part of the return data from an external call to memory. The variable length buffer is created empty for each new call-frame. Previously, the size of the return data had to be specified in advance in the call parameters. 4 | 5 | However under Solidity's implementation, up until at least `0.8.26`, the entirety of this return data is automatically copied from the buffer into memory. This is true even when using a Solidity low-level call with the omission of the `bytes memory data` syntax. 6 | 7 | Consider the following example: 8 | 9 | ```solidity 10 | pragma solidity 0.8.26; 11 | 12 | contract Attacker { 13 | function returnExcessData() external pure returns (string memory) { 14 | revert("Passing in excess data that the Solidity compiler will automatically copy to memory"); // Both statements can return unbounded data 15 | return "Passing in excess data that the Solidity compiler will automatically copy to memory"; 16 | } 17 | } 18 | 19 | 20 | contract Victim { 21 | function callAttacker(address attacker) external returns (bool) { 22 | (bool success, ) = attacker.call{gas: 2500}(abi.encodeWithSignature("returnExcessData()")); 23 | return success; 24 | } 25 | } 26 | ``` 27 | 28 | In the above example one can observe that even though the `Victim` contract has not explicitly requested `bytes memory data` to be returned, and has furthermore given the external call a gas stipend of 2500, Solidity will still invoke `RETURNDATACOPY` during the top-level call-frame. This means the `Attacker` contract, through revert or return, can force the `Victim` contract to consume unbounded gas during their own call-frame and not that of the `Attacker`. Given that memory gas costs grow exponentially after 23 words, this attack vector has the potential to prevent certain contract flows from being executed due to an `Out of Gas` error. Examples of vulnerable contract flows include unstaking or undelegating funds where a callback is involved. Here the user may be prevented from unstaking or undelegating their funds, because the transaction reverts due to insufficient gas. 29 | 30 | ### Mitigation 31 | 32 | The recommended mitigation approach is to use Yul to make the low-level call, whilst only allowing bounded return data. This method completely cuts off the attack vector for any arbitrary external call. 33 | 34 | Consider the following example from EigenLayer's original mainnet `DelegationManager.sol` contract. In this contract, delegators could delegate and undelegate their restaked assets to a manager, and each of these delegation flows had its own callback hook to an arbitrary external contract the manager specified. However the manager could use their arbitrary external contract to return unbounded data, causing the delegator to run out of gas, and thus not be able to undelegate their assets from that manager. 35 | 36 | Therefore to mitigate this griefing risk entirely, EigenLayer used a Yul call, where they limit the return data size to 1 word. If the external manager contract tries to return any more data than this, the excess of 32 bytes simply won't be copied to memory. 37 | 38 | ```solidity 39 | function _delegationWithdrawnHook( 40 | IDelegationTerms dt, 41 | address staker, 42 | IStrategy[] memory strategies, 43 | uint256[] memory shares 44 | ) 45 | internal 46 | { 47 | /** 48 | * We use low-level call functionality here to ensure that an operator cannot maliciously make this function fail in order to prevent undelegation. 49 | * In particular, in-line assembly is also used to prevent the copying of uncapped return data which is also a potential DoS vector. 50 | */ 51 | // format calldata 52 | bytes memory lowLevelCalldata = abi.encodeWithSelector(IDelegationTerms.onDelegationWithdrawn.selector, staker, strategies, shares); 53 | // Prepare memory for low-level call return data. We accept a max return data length of 32 bytes 54 | bool success; 55 | bytes32[1] memory returnData; 56 | // actually make the call 57 | assembly { 58 | success := call( 59 | // gas provided to this context 60 | LOW_LEVEL_GAS_BUDGET, 61 | // address to call 62 | dt, 63 | // value in wei for call 64 | 0, 65 | // memory location to copy for calldata 66 | add(lowLevelCalldata, 32), 67 | // length of memory to copy for calldata 68 | mload(lowLevelCalldata), 69 | // memory location to copy return data 70 | returnData, 71 | // byte size of return data to copy to memory 72 | 32 73 | ) 74 | } 75 | // if the call fails, we emit a special event rather than reverting 76 | if (!success) { 77 | emit OnDelegationWithdrawnCallFailure(dt, returnData[0]); 78 | } 79 | } 80 | ``` 81 | 82 | ### Sources 83 | 84 | - [Solidity Issue #12306](https://github.com/ethereum/solidity/issues/12306) 85 | - [ExcessivelySafeCall Repository](https://github.com/nomad-xyz/ExcessivelySafeCall) 86 | - [DelegationManager.sol (lines 259-299)](https://github.com/Layr-Labs/eigenlayer-contracts/blob/0139d6213927c0a7812578899ddd3dda58051928/src/contracts/core/DelegationManager.sol#L259-L299) -------------------------------------------------------------------------------- /vulnerabilities/reentrancy.md: -------------------------------------------------------------------------------- 1 | ## Reentrancy 2 | 3 | Reentrancy is an attack that can occur when a bug in a contract may allow a malicious contract to reenter the contract unexpectedly during execution of the original function. This can be used to drain funds from a smart contract if used maliciously. Reentrancy is likely the single most impactful vulnerability in terms of total loss of funds by smart contract hacks, and should be considered accordingly. [List of reentrancy attacks](https://github.com/pcaversaccio/reentrancy-attacks) 4 | 5 | ### External calls 6 | 7 | Reentrancy can be executed by the availability of an external call to an attacker controlled contract. External calls allow for the callee to execute arbitrary code. The existence of an external call may not always be obvious, so it's important to be aware of any way in which an external call may be executed in your smart contracts. 8 | 9 | ##### ETH transfers 10 | 11 | When Ether is transferred to a contract address, it will trigger the `receive` or `fallback` function, as implemented in the contract. An attacker can write any arbitrary logic into the `fallback` method, such that anytime the contract receives a transfer, that logic is executed. 12 | 13 | ##### `safeMint` 14 | 15 | One example of a hard to spot external call is OpenZeppelin's [`ERC721._safeMint`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/3f610ebc25480bf6145e519c96e2f809996db8ed/contracts/token/ERC721/ERC721.sol#L244) & [`ERC721._safeTransfer`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/3f610ebc25480bf6145e519c96e2f809996db8ed/contracts/token/ERC721/ERC721.sol#L190) functions. 16 | 17 | ```solidity 18 | /** 19 | * @dev Same as {xref-ERC721-_safeMint-address-uint256-}[`_safeMint`], with an additional `data` parameter which is 20 | * forwarded in {IERC721Receiver-onERC721Received} to contract recipients. 21 | */ 22 | function _safeMint( 23 | address to, 24 | uint256 tokenId, 25 | bytes memory _data 26 | ) internal virtual { 27 | _mint(to, tokenId); 28 | require( 29 | _checkOnERC721Received(address(0), to, tokenId, _data), 30 | "ERC721: transfer to non ERC721Receiver implementer" 31 | ); 32 | } 33 | ``` 34 | 35 | The function is titled `_safeMint` because it prevents tokens from being unintentionally minted to a contract by checking first whether that contract has implemented ERC721Receiver, i.e. marking itself as a willing recipient of NFTs. This all seems fine, but `_checkOnERC721Received` is an external call to the receiving contract, allowing arbitrary execution. 36 | 37 | ### Single function reentrancy 38 | 39 | A single function reentrancy attack occurs when a vulnerable function is the same function that an attacker is trying to recursively call. 40 | 41 | ```solidity 42 | // UNSECURE 43 | function withdraw() external { 44 | uint256 amount = balances[msg.sender]; 45 | (bool success,) = msg.sender.call{value: balances[msg.sender]}(""); 46 | require(success); 47 | balances[msg.sender] = 0; 48 | } 49 | ``` 50 | 51 | Here we can see that the balance is only modified after the funds have been transferred. This can allow a hacker to call the function many times before the balance is set to 0, effectively draining the smart contract. 52 | 53 | ### Cross-function reentrancy 54 | 55 | A cross-function reentrancy attack is a more complex version of the same process. Cross-function reentrancy occurs when a vulnerable function shares state with a function that an attacker can exploit. 56 | 57 | ```solidity 58 | // UNSECURE 59 | function transfer(address to, uint amount) external { 60 | if (balances[msg.sender] >= amount) { 61 | balances[to] += amount; 62 | balances[msg.sender] -= amount; 63 | } 64 | } 65 | 66 | function withdraw() external { 67 | uint256 amount = balances[msg.sender]; 68 | (bool success,) = msg.sender.call{value: balances[msg.sender]}(""); 69 | require(success); 70 | balances[msg.sender] = 0; 71 | } 72 | ``` 73 | 74 | In this example, a hacker can exploit this contract by having a fallback function call `transfer()` to transfer spent funds before the balance is set to 0 in the `withdraw()` function. 75 | 76 | ### Read-only Reentrancy 77 | 78 | Read-only reentrancy is a novel attack vector in which instead of reentering into the same contract in which state changes have yet to be made, an attacker reenters into another contract which reads from the state of the original contract. 79 | 80 | ```solidity 81 | // UNSECURE 82 | contract A { 83 | // Has a reentrancy guard to prevent reentrancy 84 | // but makes state change only after external call to sender 85 | function withdraw() external nonReentrant { 86 | uint256 amount = balances[msg.sender]; 87 | (bool success,) = msg.sender.call{value: balances[msg.sender]}(""); 88 | require(success); 89 | balances[msg.sender] = 0; 90 | } 91 | } 92 | 93 | contract B { 94 | // Allows sender to claim equivalent B tokens for A tokens they hold 95 | function claim() external nonReentrant { 96 | require(!claimed[msg.sender]); 97 | balances[msg.sender] = A.balances[msg.sender]; 98 | claimed[msg.sender] = true; 99 | } 100 | } 101 | ``` 102 | 103 | As we can see in the above example, although both functions have a nonReentrant modifier, it is still possible for an attacker to call `B.claim` during the callback in `A.withdraw`, and since the attackers balance was not yet updated, execution succeeds. 104 | 105 | ### Reentrancy prevention 106 | 107 | The simplest reentrancy prevention mechanism is to use a [`ReentrancyGuard`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/ReentrancyGuard.sol), which allows you to add a modifier, e.g. `nonReentrant`, to functions which may otherwise be vulnerable. Although effective against most forms of reentrancy, it's important to understand how read-only reentrancy may be used to get around this and to always use the **checks-effects-interactions pattern**. 108 | 109 | For optimum security, use the **checks-effects-interactions pattern**. This is a simple rule of thumb for ordering smart contract functions. 110 | 111 | The function should begin with *checks*, e.g. `require` and `assert` statements. 112 | 113 | Next, the *effects* of the contract should be performed, i.e. state modifications. 114 | 115 | Finally, we can perform *interactions* with other smart contracts, e.g. external function calls. 116 | 117 | This structure is effective against reentrancy because when an attacker reenters the function, the state changes have already been made. For example: 118 | 119 | ```solidity 120 | function withdraw() external { 121 | uint256 amount = balances[msg.sender]; 122 | balances[msg.sender] = 0; 123 | (bool success,) = msg.sender.call{value: balances[msg.sender]}(""); 124 | require(success); 125 | } 126 | ``` 127 | 128 | Since the balance is set to 0 before any interactions are performed, if the contract is called recursively, there is nothing to send after the first transaction. 129 | 130 | 131 | Examples from: https://medium.com/coinmonks/protect-your-solidity-smart-contracts-from-reentrancy-attacks-9972c3af7c21 132 | 133 | 134 | ### Sources 135 | 136 | - [Reentrancy Attacks on Smart Contracts: Best Practices for Pentesters](https://consensys.github.io/smart-contract-best-practices/attacks/reentrancy/) 137 | - [Reentrancy attack on Smart Contracts: How to identify the exploitable and an example of an attack](https://medium.com/@gus_tavo_guim/reentrancy-attack-on-smart-contracts-how-to-identify-the-exploitable-and-an-example-of-an-attack-4470a2d8dfe4) 138 | - [Protect Your Solidity Smart Contracts From Reentrancy Attacks](https://medium.com/coinmonks/protect-your-solidity-smart-contracts-from-reentrancy-attacks-9972c3af7c21) 139 | -------------------------------------------------------------------------------- /vulnerabilities/hash-collision.md: -------------------------------------------------------------------------------- 1 | ## Hash Collision when using `abi.encodePacked()` with Multiple Variable-Length Arguments 2 | 3 | In Solidity, the `abi.encodePacked()` function is used to create tightly packed byte arrays which can then be hashed using `keccak256()` 4 | 5 | However, this function can be dangerous when used with multiple variable-length arguments because it can lead to hash collisions. These collisions can potentially be exploited in scenarios such as signature verification, allowing attackers to bypass authorization mechanisms. 6 | 7 | **Hash Collision** is a situation where two different sets of inputs produce the same hash output. In this context, a hash collision can occur when using `abi.encodePacked()` with multiple variable-length arguments, allowing an attacker to craft different inputs that produce the same hash. 8 | 9 | ## Understanding the vulnerability 10 | 11 | When `abi.encodePacked()` is used with multiple variable-length arguments (such as arrays and strings), the packed encoding does not include information about the boundaries between different arguments. This can lead to situations where different combinations of arguments result in the same encoded output, causing hash collisions. 12 | 13 | For example, consider the following two calls to `abi.encodePacked()`: 14 | 15 | ```solidity 16 | abi.encodePacked(["a", "b"], ["c", "d"]) 17 | ``` 18 | 19 | ```solidity 20 | abi.encodePacked(["a"], ["b", "c", "d"]) 21 | ``` 22 | 23 | Both calls could potentially produce the same packed encoding because `abi.encodePacked()` simply concatenates the elements without any delimiters! 24 | 25 | Consider the below example for strings: 26 | 27 | ```solidity 28 | abi.encodePacked("foo", "bar") == abi.encodePacked("fo", "obar") 29 | ``` 30 | 31 | Strings in Solidity are dynamic types and when they are concatenated using `abi.encodePacked()`, there is no delimiter between them to mark their boundaries, which can lead to hash collisions. 32 | 33 | As a matter of fact, the below warning is taken as it is straight from the [official solidity language documentation](https://docs.soliditylang.org/en/latest/abi-spec.html#non-standard-packed-mode) regarding the same. 34 | 35 | 36 | > [!WARNING] 37 | > If you use `keccak256(abi.encodePacked(a, b))` and both `a` and `b` are dynamic types, it is easy to craft collisions in the hash value by moving parts of `a` into `b` and vice-versa. 38 | > More specifically, `abi.encodePacked("a", "bc") == abi.encodePacked("ab", "c")`. If you use `abi.encodePacked` for signatures, authentication or data integrity, make sure to always use the same types and check that at most one of them is dynamic. Unless there is a compelling reason, `abi.encode` should be preferred. 39 | 40 | 41 | ## Sample Code Analysis 42 | 43 | 44 | ```solidity 45 | /// INSECURE 46 | function addUsers(address[] calldata admins, address[] calldata regularUsers, bytes calldata signature) external { 47 | if (!isAdmin[msg.sender]) { 48 | bytes32 hash = keccak256(abi.encodePacked(admins, regularUsers)); 49 | address signer = hash.toEthSignedMessageHash().recover(signature); 50 | require(isAdmin[signer], "Only admins can add users."); 51 | } 52 | for (uint256 i = 0; i < admins.length; i++) { 53 | isAdmin[admins[i]] = true; 54 | } 55 | for (uint256 i = 0; i < regularUsers.length; i++) { 56 | isRegularUser[regularUsers[i]] = true; 57 | } 58 | } 59 | ``` 60 | 61 | In the provided sample code above, the `addUsers` function uses `abi.encodePacked(admins, regularUsers)` to generate a hash. An attacker could exploit this by rearranging elements between the `admins` and `regularUsers` arrays, resulting in the same hash and thereby bypassing authorization checks. 62 | 63 | ```solidity 64 | /// INSECURE 65 | function verifyMessage(string calldata message1, string calldata message2, bytes calldata signature) external { 66 | bytes32 hash = keccak256(abi.encodePacked(message1, message2)); 67 | address signer = hash.toEthSignedMessageHash().recover(signature); 68 | require(isAuthorized[signer], "Unauthorized signer"); 69 | } 70 | ``` 71 | 72 | The above function `verifyMessage()` could easily be exploited as below:- 73 | 74 | ```solidity 75 | verifyMessage("hello", "world", signature); 76 | ``` 77 | or 78 | 79 | ```solidity 80 | verifyMessage("hell", "oworld", signature); 81 | ``` 82 | 83 | or a variation of the string `hello` `world` 84 | 85 | All variations of the string `hello` `world` passed to `verifyMessage()` would produce the same hash, potentially allowing an attacker to bypass the authorization check if they can provide a valid signature for their manipulated inputs. 86 | 87 | **Fixed Code Using Single User:** 88 | 89 | ```solidity 90 | function addUser(address user, bool admin, bytes calldata signature) external { 91 | if (!isAdmin[msg.sender]) { 92 | bytes32 hash = keccak256(abi.encodePacked(user)); 93 | address signer = hash.toEthSignedMessageHash().recover(signature); 94 | require(isAdmin[signer], "Only admins can add users."); 95 | } 96 | if (admin) { 97 | isAdmin[user] = true; 98 | } else { 99 | isRegularUser[user] = true; 100 | } 101 | } 102 | ``` 103 | 104 | This approach eliminates the use of variable-length arrays, thus avoiding the hash collision issue entirely by dealing with a single user at a time. 105 | 106 | **Fixed Code Using Fixed-Length Arrays:** 107 | 108 | ```solidity 109 | function addUsers(address[3] calldata admins, address[3] calldata regularUsers, bytes calldata signature) external { 110 | if (!isAdmin[msg.sender]) { 111 | bytes32 hash = keccak256(abi.encodePacked(admins, regularUsers)); 112 | address signer = hash.toEthSignedMessageHash().recover(signature); 113 | require(isAdmin[signer], "Only admins can add users."); 114 | } 115 | for (uint256 i = 0; i < admins.length; i++) { 116 | isAdmin[admins[i]] = true; 117 | } 118 | for (uint256 i = 0; i < regularUsers.length; i++) { 119 | isRegularUser[regularUsers[i]] = true; 120 | } 121 | } 122 | ``` 123 | 124 | In this version, fixed-length arrays are used, which mitigates the risk of hash collisions since the encoding is unambiguous. 125 | 126 | 127 | ## Remediation Strategies 128 | 129 | To prevent this type of hash collision, the below remediation strategies can be employed: 130 | 131 | 1. **Avoid Variable-Length Arguments**: Avoid using `abi.encodePacked()` with variable-length arguments such as arrays and strings. Instead, use fixed-length arrays to ensure the encoding is unique and unambiguous. 132 | 133 | 2. **Use `abi.encode()` Instead**: Unlike `abi.encodePacked()`, `abi.encode()` includes additional type information and length prefixes in the encoding, making it much less prone to hash collisions. Switching from `abi.encodePacked()` to `abi.encode()` is a simple yet effective fix. 134 | 135 | > [!IMPORTANT] 136 | > Replay Protection does not protect against possible hash collisions! 137 | > 138 | > It is listed here as a defense in depth strategy and SHOULD NOT be solely relied upon to protect against said vulnerability 139 | 140 | 3. **Replay Protection**: Implement replay protection mechanisms to prevent attackers from reusing valid signatures. This can involve including nonces or timestamps in the signed data. However, this does not completely eliminate the risk of hash collisions but adds an additional layer of security. More on this can be found [here](./missing-protection-signature-replay.md) 141 | 142 | 143 | ## Sources 144 | - [Smart Contract Weakness Classification #133](https://swcregistry.io/docs/SWC-133/) 145 | - [Solidity Non-standard Packed Mode](https://docs.soliditylang.org/en/latest/abi-spec.html#non-standard-packed-mode) 146 | --------------------------------------------------------------------------------