This repository contains my ultimate solidity attack vectors compilation.
I will be compiling all solidity attack vectors that I come across with.
thanks to transmisions11/Solcurity for a kickstart :)
-
Solcurity - Solidity Attack Vectors
Quillhash - Defi Attack Vectors
Quillhash - NFT Attack Vectors
Quillhash - smart-contract-vulnerabilities by @0xKaden
- Caviar AMM December 2022
- Caviar AMM April 2023
- ENS November 2022
- @pashovkrum Bloom Protocol Report May, 2023
- @pashovkrum IPNFT - intellectual properties NFTs & fundraises
- Trust Security AlphaFinanceLab/stella-arbitrum-private- contract
- Approach : Contains the general points during auditing.
- General Entity : Common questions arise while observing specific term in the smart contract.
- Variables : Points related to state variables.
- Structs : Points related to structs.
- Functions : Points related to functions.
- Modifiers : Points related to modifiers.
- Code : Some good practices to avoid any vulnerability.
- Unexpected outputs : Some unexpected implementations and outputs related to token standards
- External Call : Points related to External Call.
- Static Call : Points related to Static Call.
- Events : Points related to Events.
- Contract : Some points related to the whole contract
- Project : Best practices during building a project.
- Defi : Contains some DeFi related vulnerabilities.
- After Transaction : Security issues that can arise after the transaction has been submitted to the mempool.
- NFT : Issues specific to NFTs.
- Read the project's docs, specs, and whitepaper to understand what the smart contracts are meant to do.
- Construct a mental model of what you expect the contracts to look like before checking out the code.
- Glance over the contracts to get a sense of the project's architecture.
- Compare the architecture to your mental model. Look into areas that are surprising.
- Identify relevant global and state variables, functions, equations that are involved in the contract.
- List all the invariants related to them and try to find a way to break them to get a loop hole in the implementation.
- Look at areas that interface with external contracts and ensure all assumptions about them are valid.
- Split the contract as the functions and variables interacting with the external contracts or not.
- Try to get what are the possibilities during different states of the contract
- when it is freshly deployed,
- when it has high amount of each token and every combination,
- when it is has some bool value come to true which is changing the state of the contract,
- try to switch between every if and else condition and also try in all ranges of the variables present
- Do a generic line-by-line review of the contracts.
- Do another review from the perspective of every actor in the threat model.
- Glance over the project's tests + code coverage and look deeper at areas lacking coverage.
- Run static analysers and review their output.
- Look at related projects and their audits to check for any similar issues or oversights.
- Try to figure out as many as expected invariants in the contract after getting its context.
- Try to avoid
transaction order dependencein the code or find a way to deal with it. - Try to anticipate what will occur when governance turns evil (this may be the case of the RUG PULL, EXIT SCAMS).
- Comment the "why" as much as possible.
- Comment the "what" if using obscure syntax or writing unconventional code.
- Comment explanations + example inputs/outputs next to complex and fixed point math.
- Comment explanations wherever optimizations are done, along with an estimate of much gas they save.
- Comment explanations wherever certain optimizations are purposely avoided, along with an estimate of much gas they would/wouldn't save if implemented.
- We should always note all the privileges that are provided to any role and what actually the role can do, any difference in these two will be a vulnerability.
- Also any role should not have the ability to take away all the funds into the contract, or any role that make the protocol centralised.
- Will the contract run the same if this entity is removed?
- Will this entity be replaced with some alternative code?
- Will this entity be used by the admin to do some exploit(making the protocol apparently centralised)?
- Is the entity opening a path for arbitrary interaction with the contract?
- Is the visibility set? Can it be more specific such as
external,internal,private? - Can it be
constant,immutable? - Is the purpose of the variable and other important information documented using
natspec? - Can it be packed with an adjacent storage variable?
- Can it be packed in a struct with more than 1 other variable?
- Use full 256 bit types unless packing with other variables.
- If it's a public array, is a separate function provided to return the full array?
- Check that the size of the array to be limited, otherwise it may lead to gas shortage to complete the transaction.
- Only use
privateto intentionally prevent child contracts from accessing the variable, preferinternalfor flexibility. - Uninitialized local storage variables(variables that take their value from a state variable) can point to unexpected storage locations in the contract, which can lead to intentional or unintentional vulnerabilities, so mark them as memory, calldata and storage as per the requirement.
- Is a struct necessary? Can the variable be packed raw in storage?
- Are its fields packed together (if possible)?
- Is the purpose of the struct and all fields documented using natspec?
- Should it be
externalorinternal? - Should it be
payable? - Can the function be front-runned?
- Can it be combined with another similar function?
- Validate all parameters are within safe bounds, even if the function can only be called by a trusted users.
- Always make sure that the argument passed is a valid argument/ behaves as expected in its full range of taking values.
- Are the multiple arrays taken have same length?
- Is the
checksbeforeeffectspattern followed? (SWC-107) - Is the
updatebeforecallpattern followed? (Reentrancy) Sometimes even the modifier can not save from reentrancy. - Are the correct modifiers applied, such as
onlyOwner/requiresAuth? - Are the
modifiers(if more than one) written in funtion in correct order, because the change in order will change the code? - Write down and test invariants about state before a function can run correctly.
- Write down and test invariants about the return or any changes to state after a function has run and try to include all edge cases as input.
- Take care when naming functions, because people will assume behaviour based on the name.
- If a function is intentionally unsafe (to save gas, etc), use an unwieldy name to draw attention to its risk.
- Are all arguments, return values, side effects and other information documented using
natspec? - Only use
privateto intentionally prevent child contracts from calling the function, preferinternalfor flexibility. - Use
virtualif there are legitimate (and safe) instances where a child contract may wish to override the function's behaviour. - Are return values always assigned?, sometimes not assigning values is better.
- Try not to use
msg.value, after its value has been used as this can cause the loss of funds of the contract.msg.valuecan be used in case of fees payment which is very small and protocol exclusive.
- Are no storage updates made (except in a reentrancy lock)?
- Are
external callsavoided? - Is the purpose of the modifier and other important information documented using
natspec? - Always remember that
modifiersincrease the codesize so use them wisely.
- Using SafeMath or 0.8 checked math? (SWC-101)
- Are any storage slots read multiple times?
- Implementation should be consistent with the documentation, whats written in docs should be implemented in the contract.
- Are any unbounded loops/arrays used that can cause DoS? (SWC-128)
- Use
block.timestamponly for long intervals. (SWC-116) - Don't use block.number for elapsed time. (SWC-116)
- Do not update the length of an array while iterating over it.
- Don't use
blockhash(), etc for randomness. (SWC-120) - Are signatures protected against replay with a nonce and
block.chainid? (SWC-121) - Ensure all signatures use EIP-712. (SWC-117 SWC-122)
- Output of
abi.encodePacked()shouldn't be hashed if using >2 dynamic types. Prefer usingabi.encode()in general. (SWC-133) - Don't use any arbitrary data while using the assembly. (SWC-127)
- Don't assume a specific ETH balance. (SWC-132)
- Private data isn't private, it can be accessed. (SWC-136)
- Updating a struct/array in memory won't modify it in storage.
- Never shadow state variables. (SWC-119)
- Try not to mutate function parameters.
- Is calculating a value on the fly cheaper than storing it?
- Are all state variables read from the correct contract (master vs. clone)?
- Are comparison operators used correctly (
>,<,>=,<=), especially to prevent off-by-one errors? - Are logical operators used correctly (
==,!=,&&,||,!), especially to prevent off-by-one errors? - Always multiply before dividing, unless the multiplication could overflow.
- Are magic numbers replaced by a constant with an intuitive name?
- If the recipient of ETH had a fallback function that reverted, could it cause DoS? (SWC-113)
- Use SafeERC20 or check return values safely.
- Don't use
msg.valueif recursive delegatecalls are possible (like if the contract inheritsMulticall/Batchable). - Don't assume
msg.senderis always a relevant user. - Don't use
assert()unless for fuzzing or formal verification. (SWC-110) - Don't use
tx.originfor authorization. (SWC-115) - Don't use
address.transfer()oraddress.send(). Use.call.value(...)("")instead. (SWC-134) - When using low-level calls, ensure the contract exists before calling.
- When calling a function with many parameters, use the named argument syntax.
- Do not use assembly for create2. Prefer the modern salted contract creation syntax.
- Do not use assembly to access chainId or contract code/size/hash. Prefer the modern Solidity syntax.
- Use the
deletekeyword when setting a variable to a zero value (0,false,"", etc). - Use
uncheckedblocks where overflow/underflow is impossible, or where an overflow/underflow is unrealistic on human timescales (counters, etc). Comment explanations whereveruncheckedis used, along with an estimate of how much gas it saves (if relevant). - Do not depend on Solidity's arithmetic operator precedence rules. In addition to the use of parentheses to override default operator precedence, parentheses should also be used to emphasise it.
- Expressions passed to logical/comparison operators (
&&/||/>=/==/etc) should not have side-effects. - Wherever arithmetic operations are performed that could result in precision loss, ensure it benefits the right actors in the system, and document it with comments.
- Document the reason why a reentrancy lock is necessary whenever it's used with an inline or
@devnatspec comment. - When fuzzing functions that only operate on specific numerical ranges use modulo to tighten the fuzzer's inputs (such as
x = x % 10000 + 1to restrict from 1 to 10,000). - Use ternary expressions to simplify branching logic wherever possible.
- When operating on more than one address, ask yourself what happens if they're the same.
- Can someone without spending other then gas fees change the state of the contract.
- Always check the number of loop iterations should be bounded by a small finite number other wise the transaction will run out of gas.
- Always check for the return datatype of the called contract function. Example: in ERC20 implementations, the transfer functions are not consistent with the value they return(some return the bool while others revert which can cause problems)
- You can always convert
booltorevertby usingrequire. - Similar to the above, global
transfermethod reverts while thesendgives the bool value which sometimes causes problems - Don't use
extcodesizeto gain the knowledge of whether themsg.senderis EOA as any contract calling the function while staying in the constructor can easily act as an EOA. - Try to monitor the expected and actual length of the array.
- Always try to be consistent with the interface contract otherwise the call will lead to the fallback.
- Making a new owner is a crucial thing, so a new function to accept the ownership should be made so that the ownership don't go in the hands of some wrong person or a smart contract which can not do anything.
- In Solidity any address can be casted into specific contract, even if the contract at the address is not the one being casted. This can be exploited to hide malicious code.
- don't use
erecoverandsignatureto verify the user as these cause signature malleability. - delete every entry of the mapping before deleting the mapping itself, otherwise the getter function will still work by giving all the mapping values
- Look out for signature replay attacks.
- Use underscores or constants for number literals for better readability and also for unexpected human error.
- Use bytes.concat() instead of abi.encodePacked(), since this is preferred since 0.8.4
- Any inconsistency in formula for calculation may cause the loss of the funds and also minting additional funds,
example can be use of Math.min(a, b) which change suddenly when the condition changes. - Don't assume the implementations of ERC20, ERC721 tokens in their contracts, such as decimals, approve functions etc., coding using this assumption will lead to the casting errors
- Look for the statements that can be skipped and still takes to the same blockchain state, for example some external call without any return values, some non-relevant require statements.
- Try to read all the ERC20 Implementations in scope as their definitions can be different from what is expected.
Round Upshould be done while taking the tokens in so that no one can be privileged while depositing a lower amount.Round downshould be done while transferinng tokens from protocol to user so that no user can get the same value while having lower deposit.- Use
PULLoverPUSHwhile updating the state variables to mitigate the inclusion of blacklisted entities to become active. This also uses gas only whenever necessary - Try not to use the
percentage, because it introduces the division and then rounding occurs. Also include a 100% cap while including a percentage. - It is necessary to make the lines in constructor in proper order, this really affect the initial state of the protocol. Example. a function called inside the constructor takes value of an uninitialized variable, hence will fail to give correct output.
- Most price feeds use
Chainlinkas their price feed which sometimes return the values at8 decimalnumbers, so while scaling the output with a general formula using 1e12 or 1e18 will not be applicable. - Some tokens like
PAXG,USDThave fee-on-transfer in-built which makes them transfer less tokens than the argument passed. So to get the exact value of transfer tokens we have to fetch the balance of the receiving contract twice(one before transfer and one after) and also a non-reentrant to protect against ERC-777 tokens - Tokens like
USDT,KNChave a approval-race-protection mechanism which usesallowancewhich is either set to0ortype(uint256).max, at any other value, the safe-approval willrevertinstead of giving abool. So we have to useforceApproval Allowanceto mitigate this problem and to generalize any token approval in our protocol.USDTalso don't have anincreaseAllowance()function. - Decimals are not fixed to 18 for all ERC20 implementations, such as
GUSD-Gemini Dollarhas only 2 decimals. - There are implementations of ERC721 that revert when calling the
setApprovalForAllfunction more than one times, this is because the function has a checkrequire(_tokenOperator[msg.sender][_operator] != _approved). Example isAxieERC721 Token. - Chainlink's
latestRoundData()is used, then there should be a check if the return value indicates old data. Otherwise this could lead to old prices according to the Chainlink documentation. Also if any variable is used to make sure that the data is not outdated, then while using the two different price feeds, we have to make sure that these two price feeds are updated at comparable amounts of time other wise the differene between their update time will lead to unexpected changes. - Different chains have different block mining time which poses a vulnerability when writing the same code for all the chains while relating the number of blocks and the timestamp.
- Using
solmate safeTransferLib, one should also make a function to check whether the token contract exist or not, because this is not included in that library - A problem with using only
approvefunction but not theincreaseAllowanceis that, If A approves B 5 tokens and B don't use them, Now, If A approves B 10 tokens to increase the approve value from 5 to 10 tokens, so that B can spend 10 tokens, now B can front run that 10 token transaction to spend both 5 and 10 tokens.
- Is an external contract call actually needed?
- Avoid delegatecall wherever possible, especially to external (even if trusted) contracts. (SWC-112)
- If there is an error, could it cause DoS? Like
balanceOf()reverting. (SWC-113) - Would it be harmful if the call reentered into the current function?
- Would it be harmful if the call reentered into another function?
- Is the result checked and errors dealt with? (SWC-104)
- What if it uses all the gas provided?
- Could it cause an out-of-gas in the calling contract if it returns a massive amount of data?
- If you are calling a particular function, do not assume that
successimplies that the function exists (phantom functions). - Its best to be stateless while doing an external delegate call.
- Always assume that the external call will fail, now code accordingly.
- Try avoiding taking arbitrary input or calldata input for a function that does external call which can make the EOA make the calls in the behalf of the contract.
- The external calls from a contract can be made to be failed and still be made the function continue if the external call returns a bool, the attacker can just give very enough gas to make the sub-call(call from a contract function to another contract) fail.(Insufficient Gas Greifing)
- Is an external contract call actually needed?
- Is it actually marked as view in the interface?
- If there is an error, could it cause DoS? Like
balanceOf()reverting. (SWC-113) - If the call entered an infinite loop, could it cause DoS?
- Is this call supporting Reentrancy? Is this call reading the updated values OR outdated values?
- Should any fields be indexed?
- Is the creator of the relevant action included as an indexed field?
- Do not index dynamic types like strings or bytes.
- Is when the event emitted and all fields documented using natspec?
E5- Are all users/ids that are operated on in functions that emit the event stored as indexed fields?- Avoid function calls and evaluation of expressions within event arguments. Their order of evaluation is unpredictable.
- Events should be made for every important change in state made through the contract, so they can be read off-chain.
- Avoid event spamming where there are events emitted even when there is e.g. zero claim amount, zero token transfer, as this will affect off-chain event tracking.
- Use an SPDX license identifier.
- Are events emitted for every storage mutating function?
- Check for correct inheritance, keep it simple and linear. (SWC-125)
- Use a
receive() external payablefunction if the contract should accept transferred ETH. - Write down and test invariants about relationships between stored state.
- Is the purpose of the contract and how it interacts with others documented using natspec?
- The contract should be marked
abstractif another contract must inherit it to unlock its full functionality. - Emit an appropriate event for any non-immutable variable set in the constructor that emits an event when mutated elsewhere.
- Avoid over-inheritance as it masks complexity and encourages over-abstraction.
- Always use the named import syntax to explicitly declare which contracts are being imported from another file.
- Group imports by their folder/package. Separate groups with an empty line. Groups of external dependencies should come first, then mock/testing contracts (if relevant), and finally local imports.
- Summarize the purpose and functionality of the contract with a
@noticenatspec comment. Document how the contract interacts with other contracts inside/outside the project in a@devnatspec comment. - Malicious actors can use the Right-To-Left-Override unicode character to force RTL text rendering and confuse users as to the real intent of a contract.
- Try to take into account the c3 linearization when inheriting from two contracts that contain same function with different implementations (diamond problem)
- The callable functions in a contract are not only the ones visible in the contract code but also the ones which are inherited but are not mentioned in the code itself.
- Its a good practice to include the headers
- The functions should be grouped in the following order as given in the solidity style guide for the auditing process should be smooth
{ constructor, receive function (if exists), fallback function (if exists), external, public, internal, private, view and pure functions last } - Always look for making an extra function(claim) if there is possibility of the funds to be stuck in the contract or the contract is having a receive or fallback function. This can be seen in the case of airdrops that are generally landed on the protocol contract and a claim function should be made to retrieve them.
- In the beginning after deployment of the contract, the state variables are easy to manipulate(especially in defi) since there is not much of the funds locked in the contract, and hence not very much of the funds are required to manipulate the state of the contract, this can lead to the contract being more vulnerable in start
- If the contract is an implementation of an another protocol, then to maintain the consistency, we should check all the formulas to be same in both. This can happen in the strategy protocols that makes strategy for another defi protocols but lacks giving the users same values or outputs.
- While using the proxy, Initialize the contract in the same transaction as initialization needs a call to initialixe function.
- Using same data feed of two related tokens is vulnerable, e.g. using datafeed for
USDCforDAIwill be vulnerable as if one depegs, then the other price will also be affected in the protocol. - Is the contract upgradeable? If yes, then are there any storage slots reserved?
- Use the right license (you must use GPL if you depend on GPL code, etc).
- Unit test everything.
- Fuzz test as much as possible.
- Use symbolic execution where possible.
- Run Slither/Solhint and review all findings.
- The coverage for the tests should be 100%
defi has many vulnerabilities outside solidity, so familiarize yourself with the crypto space and its trends
includes : structuring to avoid AML/CTF, token inflation, fake trends, smurfing, Interlocking Directorate
- Check your assumptions about what other contracts do and return.
- Don't mix internal accounting with actual balances.
- Don't use spot price from an AMM as an oracle.
- Do not trade on AMMs without receiving a price target off-chain or via an oracle.
- Use sanity checks to prevent oracle/price manipulation.
- Watch out for rebasing tokens. If they are unsupported, ensure that property is documented.
- Watch out for ERC-777 tokens. Even a token you trust could preform reentrancy if it's an ERC-777. ERC721 are also vulnerable.
- Watch out for fee-on-transfer tokens. If they are unsupported, ensure that property is documented.
- Watch out for tokens that use too many or too few decimals. Ensure the max and min supported values are documented.
- Be careful of relying on the raw token balance of a contract to determine earnings. Contracts which provide a way to recover assets sent directly to them can mess up share price functions that rely on the raw Ether or token balances of an address.
- If your contract is a target for token approvals, do not make arbitrary calls from user input.
- Always set a minimum deposit balance to revoke the privilege given to people depositing zero amount
- One of the best optimisations can be decreasing the impermenant loss(maybe divide the loss among more people since the overall loss can not be decreased as this will affect the price impact on the AMM)
- Check out for whether governance given to an EOA has infinite minting or approval power(to avoid rug pull, exit scams, circulating price impact)
- Look out for slippage tolerance in Defi Dex protocol, this saves from unexpected results and even protects from front running
- There is slippage cap in the functions in AMMs but there should also be the deadline set as the slippage cap gives the person assets in a specified range but the real value of the asset can be changed with time, so even if getting the same amount of token, but not at proper time can lead to bad trade.
- The main concern while swapping is getting the expected price, so during very high fluctuations, using slippage in form of percentage or deviation from the current price is not a good idea since during high fluctuations, even inside the deadline the price may be very unexpected, so the best way to use swaps is (deadline + expected price) you want rather slippage percentage or absolute difference from the current price.
- Try not to approve the token contracts which have onlyOwner functions which have the power to move the funds.
- Watch out what if someone with very much money can do(in cases of auction), in these cases a flashloan attack is likely to happen
- Functions without any protection(like onlyOwner) are vulnerable to frontrunning so consider what will happen if they are frontrunned.
- Fees is a part of many protocols, watch out for the msg.sender, fee payer, funds receiver as different users.
- In case of protocols having subscriptions, unregistered, de-registered, expired entries are also different, these should be acting according to the documentation.
Inflation attack: It is the attack in which the pool is submitted the tokens externally and now the liquidity is very high and the total supply of mint tokens is very low and hence the formula will give the minimum amount to deposit to be very high and hence DOSing for people with low money.maxSlippagevalue should not be fixed, because in case of emergency where the price is constantly dropping or increasing, the withdraw function or swap function will revert due to crossing of themaxSlippage. But, at that time the transaction should pass otherwise the funds will be stuck forever as the slippage will never come to low.- In a lending and borrowing protocol, this can be a valid finding if at some point of time, the borrower is freezed to borrow the funds or is limited to borrow comparably less funds but is able and have tokens to give collateral, as this will significantly decrease the yield of the lender.
- Watch out for all entry points for a position in a protocol for example in case of a protocol build on uniswap will have two entry points for adding liquidity, one of them is the protocol and another is through the pool. Try to investigate all the entry points and how can an entry points be used for unintended behaviour.
- The transaction data can be seen buy the miner, so don't use things like password in the transactions.
- Any smart contract using NFT contracts as input should also include a function to blacklist NFTs so that anyone can not use NFT contracts as inputs that are theft in the past