- Published on
EVM Vulnerability Types
- Authors
- Name
- Frank
Vuln Types
References
1. Improper Input Validation
- description
- when contracts fail to validate and sanitize user inputs
- prevention
- validate data types
- check boundary conditions
- consider all possible inputs including edge cases
- use fuzzing or symbolic execution
Notes
input validation
- need to check that the two tokens for a swap are expected
- otherwise can provide a different token and mess with the swap calculations
- always think about other values that can be substituted
overflow
- Overflow is when the result of an operation is higher than the maximum possible value that a uint (unsigned integer) can hold (which is 2^256 - 1).
- For instance, if you try to add 1 to the maximum possible uint, you'll get an overflow, and the result will wrap around to 0.
- Overflow is when the result of an operation is higher than the maximum possible value that a uint (unsigned integer) can hold (which is 2^256 - 1).
underflow
- Underflow is the opposite: it's when the result of an operation is less than the smallest possible value that a uint can hold (which is 0).
- For instance, if you subtract 1 from 0, you'll get an underflow, and the result will wrap around to the maximum possible uint value.
- Underflow is the opposite: it's when the result of an operation is less than the smallest possible value that a uint can hold (which is 0).
look at the tpes of variables when testing them
- e.g: a
uint
is always positive, so testing that it is>= 0
will always pass
- e.g: a
delegatecall
- overwrite shared state variables basically
- your contract is practically saying "here, -other contract- or -other library-, do whatever you want with my state"
- low-level function in Solidity
- call another contract while using the state of the calling contract
- often used with libraries, contract calls a library function while still using it's own state
- often used with upgradeable/proxy contracts
this
will still refer to the calling contractmsg.sender
andmsg.value
still keep their original values unlike a standard call
casting
uint64
function in Solidity is a type-casting function- When you have a
bytes8
value, it's actually an array of 8 bytes. - Each byte is 8 bits, so
bytes8
in total is 64 bits, same asuint64
. - So, when you apply
uint64()
to abytes8
value, Solidity interprets those 8 bytes as a single 64-bit integer. - casting from a larger integer type to a smaller one will discard the extra high-order bits
- the lest significant bits would be kept
check addresses are not set to address(0)
- Whenever you're setting or updating address-type state variables in your contract, especially those representing roles like owner, admin, or other critical functionalities, always check against the zero address.
- if address(0) is used the contract might behave unexpectedly or be rendered unusable
contract ExampleContract {
address public owner;
constructor(address _owner) {
// Add this check in
require(_owner != address(0), "Owner address cannot be zero address");
owner = _owner;
}
}
don't set a state variable to the same value again
- If a function emits an event every time a state variable is set, and that variable is set to the same value it already has, it can confuse off-chain systems or users monitoring those events.
- best to check if the values match before setting it again
- displayed names of ERC-20 tokens
- guy created a token whose name was executable JS to steal session data
- in the session data were user's private keys
- sanitise input escape output
params
are controlled by userif (block.timestamp < params.endTime) then something
- the params are entirely controlled by user
- so this is a useless check
functions that take an array as a parameter, always ask
- what if there is a default value element?
- what if there are duplicate elements?
- what if the array is empty?
- does the order of elements matter?
2. Incorrect Calculation
- description
- mathematical operations performed incorrectly
- incorrect assumptions about precision, range of values or inconsistent calculations
- also when failing to handle edge case values, overflows, underflows
- prevention
- fuzzing, symbolic execution
- use math libs
Notes
wei
- value of transactions on Ethereum network always denominated in wei
- 1 Ether = 1^18 Wei
- Ether has 18 decimal places
- use utility functions like
web3.utils.toWei('1', 'ether')
to convert Ether to Wei
contract balance
- every contract and EOA has a balance
- amount of Ether stored at that address
- it is not stored in state variables in the contract
- read balance of a contract
let balance = await web3.eth.getBalance(contractAddress);
- every contract and EOA has a balance
decimals
- ERC20 tokens use 18 decimal places
- but it's represented by integers, not floats
- so, one unit of a token is
1000000000000000000
, 1 followed by 18 zeroes
floating points
- no floating points in solidity
- whenever a function does a division the result will be a fraction
- no floating points, so no fractions, and rounded down
- can make continuous swaps and the precision loss will round down
- need to be careful with precision for calculations
casting of arithmetic operations
- issue
- when casting the result of any arithmetic operation like
int256 diff = int256(currPrice - lastPrice);
- where
currPrice
andlastPrice
areuint256
- the result is first stored in the larger type of the 2 variables and only then cast
- so, if
currPrice - lastPrice
= -1 then it will underflow in auint256
value- so instead of getting a negative number (which
uint256
can't represent), you get a very large positive number - for example, if you do
1 - 2
inuint256
, you don't get-1
; you get2**256 - 1
- when you cast this very large uint256 result to int256
- if the number is within the range of int256 it will be cast as is
- if it's larger than the maximum value of int256 it will create problems
- so instead of getting a negative number (which
rounding down division
- Solidity does not support fractions
- when performing division the result is always rounded down to the nearest whole number
- this can lead to loss of precision
- especially when numerator is less than denominator e.g: 1/2 = 0.5 rounded down to 0
- best to require that numerator is greater than denominator to avoid this issue
3. Oracle / Price Manipulation
- description
- contracts rely on external data sources to make decisions
- spot prices from exchanges can be manipulated
- pools with shallow liquidity are at higher risk
- prevention
- select trusted oracles
- staleness checks
- average pricing
- read-only reentrancy protection
- multiple data source aggregation
Notes
4. Weak Access Control
- description
- allows unauthed users to gain unauthed access to critical functions
- prevention
- role based access control mechanisms
- strong signature verification
- use tested libs
Notes
storage
- nothing in Ethereum is private
- state variables marked as private are still publicly accessible
- local variables are still publicly accessible
- on Ethereum, all data is ultimately public, its visible to someone inspecting the blockchain state at a low level
- do not store sensitive data in a contract in plain text
- usually store a cryptographic hash of the data rather than the data itself
- can read data which are state variables for a contract by reading what is stored at slot 1, 2, 3 etc.
- Web3's
getStorageAt(...)
can be used to read anything from storage - need to figure out which slot to read for each state variable
- some state vars might share a slot depending on type
- e.g;
uint8
anduint16
are smaller than 32 bytes (size of storage slot) so they can be packed together
- e.g;
- marking variable as private only prevents other contracts from accessing it
- nothing in Ethereum is private
state variable specifiers
- visibility
public
- This means the variable can be read from inside and outside the contract.
- Solidity automatically generates a getter function for
public
state variables.
internal
- The variable is visible only inside the contract where it is defined, as well as in any contracts that inherit from it.
private
- The variable is only visible inside the contract where it is defined.
- It's not visible in derived contracts.
- type
constant
- This means the variable cannot be modified after it is initialized, and it must be initialized at declaration.
immutable
- Similar to
constant
, but it can be set within the constructor. - It's value is stored in code.
- Similar to
- visibility
function specifiers
- visibility
public
- This function is part of the contract interface and can be called both internally and via messages.
- For public state variables, an automatic getter function (also public) is generated.
private
- This function is only visible within the current contract and can't be accessed from derived contracts or from outside the contract.
- accessibility
external
- This function is part of the contract interface, which can be called from other contracts and via transactions but cannot be called internally, except with
this.functionName()
.
- This function is part of the contract interface, which can be called from other contracts and via transactions but cannot be called internally, except with
internal
- This function is only accessible from the current contract and contracts that inherit from it.
- It cannot be accessed from outside the contract.
- type
virtual
- This is used when you want a function or modifier potentially overridden in the derived contract.
override
- This is used when you want to override a function or modifier defined in the parent contract.
- function
pure
- This indicates that the function does not read or modify the state.
view
- This indicates that the function does not modify the state; i.e., it does not change any state variables or write anything to the blockchain.
payable
- This modifier allows a function to receive Ether while being called.
- A function without this modifier will reject a call if it carries any Ether with it.
- visibility
slot stuffing
- can basically overwrite earlier state variables if you can store a variable at a given address in a dynamic array
- can't just stuff the array all the way up to 2^256, that's cost prohibitive
- must be able to store at an address directly in order to do it
- fixed in Solidity v0.8.0
timelock owner changes
- changes to owner are high impact
- might be an attacker changing ownership, or just a typo in new owner address
- both can lead to issues with the protocol
- use a timelock so that there is a delay before operation to change owner can be finalised
- this gives users time to inspect the changes and remove funds if necessary
msg.sender changes for
this.
function calls- in most OO languages these two are synonymous
this.fun()
andfun()
- both are called on "this"
- in Solidity,
this.fun()
results in an external call, and the msg.sender changes - so any access control based on msg.sender fails
- in most OO languages these two are synonymous
5. Replay Attacks / Signature Maleability
- description
- when an attacker replays a valid transaction or message to decieve the smart contract into performing an action more than once
- signature maleability is when a sig can be modified without invalidating it, allowing the sig to be used twice
- can be introduced when encoding data or casting between types, some bits of a value are ignored when checking the sig
- prevention
- introduce a nonce (number-used-once) which is incremented when a signature is used, preventing it from being used again
- implement proper sig verification checks such as validating the integrity and authenticity of sigs
Notes
- a signature generated by a smart contract to authorise some action
- the signature should only authorise that particular action
- an attacker might be able to see the signature
- from an event emmitted by a contract
- from transaction data
- an attacker could send the same signature to the contract again using it in a different context
- if the signature is not restrictive enough in it's authorisation
- should be restricted to least privilege necessary to carry out the action
6. Rounding Error
- description
- when contracts perform calucations involving floating point arithmetic and fail to account for precision or rounding
- errors lead to incorrect rewards calculated etc.
- precision loss
- prevention
- contracts should use fixed-point arithmetic or libs that provide precise numerical operations
- fixed-point arithmetic uses integer values to represent decimals avoiding the imprecision associated with floating-points
Notes
- division before multiplication
- division can result in rounding down errors
- to minimise any rounding errors we always want to perform multiplication before division
- division before multiplication errors cna be hidden by function calls in the equation
- e.g:
- division before multiplication
uint256 scale0 = Math.mulDiv(amount0, 1e18, liquidity) * token0Scale;
- fixed
uint256 scale0 = Math.mulDiv(amount0 * token0Scale, 1e18, liquidity);
- division before multiplication
- below wmul() and wdiv() were hiding the fact that there was Division Before Multiplication, but once the functions calls are expanded it becomes visible
- e.g:
iRate = baseVbr + utilRate.wmul(slope1).wdiv(optimalUsageRate)
// expand out wmul & wdiv to see what is actually going on
// iRate = baseVbr + utilRate * (slope1 / 1e18) * (1e18 / optimalUsageRate)
//
// now can see Division Before Multiplication:
// (slope1 / 1e18) is then multiplied by (1e18 / optimalUsageRate),
// leading to precision loss.
//
// To fix, always perform Multiplication Before Division:
// iRate = baseVbr + utilRate * slope1 / optimalUsageRate;
- even when we multiply before division we can get precision loss when dealing with small numbers
- always check the math using a very small number
- e.g: repay a loan with a tiny amount
- check if values are 0 when they shouldn't be and revert
- e.g:
if( decollateralized == 0 ) { revert("Round down to zero"); }
- e.g:
- consider a trading pool that trades a primary token against a secondary token
- these tokens could each have different precision
- e.g: DAI has 18 decimal places, USDC has only 6 decimal places
- loss of precision can occur if computation is done by combining the amounts of these two tokens with different precision
- without first scaling the precision of the secondary token to the primary tokens precision
- e.g: SUM the two tokens without their decimal places matching
- always convert all amounts into primary tokens precision before any computation
- consider a trading pool that trades a primary token against a secondary token
- accidentally scaling the token amounts more than once
- trace through code and have a look for repeatedly scaling
- expecially on larger modular codebases
mismatched precision scaling
- one module may scale precision by a token's decimals, while another module may hard-code a common value such as 1e18
- check that precision scaling is consistent, a token might not use a precision which matches the hardcoded values
// @audit Vault.vy; vault precision using token's decimals
decimals: uint256 = DetailedERC20(token).decimals()
self.decimals = decimals
/// ...
def pricePerShare() -> uint256:
return self._shareValue(10 ** self.decimals)
// @audit YearnYield; yield precision using hard-coded 1e18
function getTokensForShares(uint256 shares, address asset) public view override returns (uint256 amount) {
if (shares == 0) return 0;
// @audit should divided by vaultDecimals
amount = IyVault(liquidityToken[asset]).getPricePerFullShare().mul(shares).div(1e18);
}
- downcast overflow
- when downcasting from one type to another Solidity will not revert but overflow
- look for patterns where
require()
checkes occur before the downcast - should use the OpenZeppelin SafeCast lib
- below
- endTime will overflow for values >= 2 ** 32, resulting in the invariant check becoming invalid after the downcast
- this value might be stored and exploited later on
function errorDowncast(uint sTimeU256, uint eTimeU256)
external view returns (uint sFromU32, uint eFromU32) {
// checks before downcast conversions may not be true
// after the downcast if overflow occurs
require(eTimeU256 > sTimeU256, "End Time must > startTime ");
uint32 sTimeU32 = uint32(sTimeU256);
uint32 eTimeU32 = uint32(eTimeU256); // overflow for >= 2 ** 32
console.log("sTimeU256 : ", sTimeU256);
console.log("eTimeU256 : ", eTimeU256);
console.log("sTimeU32 : ", sTimeU32);
console.log("eTimeU32 : ", eTimeU32); // 0 for 2 ** 32
return (uint(sTimeU32), uint(eTimeU32));
}
- rounding leaks value from protocol
- in AMM and similar protocols, rounding on buying, selling and fees should always favour the protocol to prevent leaking value from the system to traders
// @audit rounding down favors traders, can leak value from protocol
protocolFee = outputValue.mulWadDown(protocolFeeMultiplier);
// fixed: round up to favor protocol and prevent value leak to traders
protocolFee = outputValue.mulWadUp(protocolFeeMultiplier);
// @audit rounding down favors traders, can leak value from protocol
tradeFee = outputValue.mulWadDown(feeMultiplier);
// fixed: round up to favor protocol and prevent value leak to traders
tradeFee = outputValue.mulWadUp(feeMultiplier);
7. Re-entrancy
- description
- allows an attacker to repeatedly call a contract before the previous call completes
- leads to unexpected state changes and unauthed fund transfers
- allows an attacker to repeatedly call a contract before the previous call completes
- prevention
- secure state management patterns
- Checks-Effects-Interactions (CEI) pattern
- state changes are made before any external calls are executed
- Checks-Effects-Interactions (CEI) pattern
- applying mutex locks
- ReentrancyGuard pattern
- secure state management patterns
Notes
re-entrancy
- contract A function calls contract B then continues processing
- contract B calls the contract A function creating a loop
- checks-effects-interactions pattern
- checks
- update the state
- call other contracts right at the end
- so the other contracts are called after the state is updated
- OpenZeppelin RenetrancyGuard
- updates variable if function entered the first time to prevent it entering again
- contract A function calls contract B then continues processing
tokens like WBTC, WETH and USDC have callbacks after transfers
- which could lead to re-entrancies
- look at the state which is not being updated
- look at where that state is used
- figure out how to manipulate that state
- figure out the math
8. Frontrunning
- description
- attacker exploits the delay between when a pending transaction is observed and its inclusion in a block
- can do this by observing the mempool for example
- it's a problem when transaction order impacts the outcome
- in a dex an attacker can observe victims pending txn to buy token at a certain price
- attacker then quickly submits own txn with higher gas to buy token at lower price before victims txn executes
- can be done by block producers themselves
- attacker exploits the delay between when a pending transaction is observed and its inclusion in a block
- prevention
- keep some data private e.g: prices or bids secrete until transaction is confirmed
- off-chain order matching
- use of flashbots
- fee optimisation to reduce likelihood of being outbid
Notes
- MEV bots
- liquidation
- sandwich attack
- arbitrage
- sniping
- frontrunning
9. Uninitialized Proxy
- description
- using proxy contracts without proper initialisation
- state variables are not properly initialised
- attackers can manipulate uninitialised storage variables to gain unauthorised access or execute unintended actions
- prevention
- initialise proxies properly
- check that sensitive data, access control permissions, critical state varaibles are initialised properly
Notes
proxies
- delegatecall
- different to get proxies right
- storage collisions when the same slot is used in the proxy from the target
- the slots for variables works both ways
- e.g: update slot 0 in proxy then use that value in the logic controller
- the slots for variables works both ways
- function clashes
- create the same function signature in the target
EIP1967
- standard for which slot to use for upgradeable contracts
- keccak256 hash of the string "eip1967.proxy.implementation" to determine the storage slot for the implementation address
initilisable
- initialise function on implementation contract for proxy
- because the constructor runs when the contract is deployed to the blockchain and cannot run again
- initialise function on implementation contract for proxy
10. Governance Attacks
- description
- manipulation or exploiting governance mechanisms
- proposals to be executed without quorum
- execute proposals without voting step
- relevant for DAOs where decision making authority is distributed among token holders
- take a flash loan of tokens to execute
- prevention
- secure and tamper resistant voting systems
- zero knowledge proofs
- multi-sig schemes
11. Edge Case Pathways
- description
- code execution paths which are not fully thought out
- magic methods etc. that are called in certain circumstances
- prevention
- unit tests to cover exception paths etc.
Notes
look at
require
statements- e.g: see if there is a way around them, if there is, the rest of the logic will be run
fallback functions
- when sending Ether to a contract and no function matches
- Solidity >0.6.0 receive function
- which is executed on a call to the contract if no other fucntions match the function identifier or no data supplied at all
- fallback
- called when a contract receives Ether without data or function is called that doesn't exist
- must be external
- can be payable or non-payable
- no arguments
- receive
- called when contract receives Ether without any data
- must be external payable
- without fallback functions, if contract receives Ether directly it will fail and Ether will be sent back to sender
pragma solidity ^0.8.0;
contract MyContract {
fallback() external payable {
// code here
}
}
pragma solidity ^0.8.0;
contract MyContract {
receive() external payable {
// code here
}
}
selfdestruct
- if a contract doesn't specify a fallback or receive function, it cannot receive Ether through regular transactions
- however, can force Ether into the contract using methods that do not invoke the fallback function e.g:
selfdestruct
- if a contract doesn't have a function to receive Ether it doesn't mean it can't hold Ether
- if another contract calls
selfdestruct
and sends it's Ether to your contract the Ether would still be recieved - if a miner includes contract address in the coinbase transaction it will send it Ether
- if another contract calls
- don't use
address(this).balance == 0
for any contract logic
gas
- paid by the EOA that is initiating the transaction
- gas usage deploying a contract
- deploy a contract actually sending a special type of txn to the network
- just like other txns, contract deployment requires gas
- gas for
transfer
andsend
- forward only a limited amount of gas (2300) which might not be enough if receiving contract has a lot of complex logic in fallback function
- gas for
call
- forwards all available gas, allows receiving contract to consume all available gas
- recommended to use
call
for sending Ether e.g:.call{value: x}("")
- can limit the gas being forwarded using
.gas(gasAmount)
- can be similar to a function call e.g:
address(contractB).call(abi.encodeWithSignature("functionName()"));
- gas for a function call e.g:
contract.functionName()
- forwards all available gas, allows receiving contract to consume all available gas
approve
andtransferFrom
- usually token holder calls
approve
to grant an allowance of tokens to the sender that they can send - the sender calls
transferFrom
to move the tokens
- usually token holder calls
gas usage
- if you use all the gas up the entire transaction will be reverted
- revert() and require() statements only undo the changes made by the current call and any sub-calls it made, and they refund the remaining gas to the caller
- if you set up an infinite loop the txn runs out of gas and the whole thing is undone/reverted
12. Misuse of API
- description
- Solidity API misused in some way
Notes
Solidity properties
msg.sender
- address of immediate caller of current function
- e.g if EOA calls contract A calls B, msg.sender is address of A
tx.origin
- address of original sender
- e.g if EOA calls contract A calls B, tx.origin is address of EOA
- use is discouraged
block.number
is a global variable in Solidity which gives you the block number of the current block being mined
Solidity low-level functions
- assembly
- more direct interaction with the EVM
- the assembly function that you can call are actually the opcodes of the EVM
- EVM is runtime environment for smart contracts
- it's a stack based virtual machine which executes opcodes
- opcodes
- lowest level commands/instructions in a virtual machine or physical CPU
- most fundamental operations the machine can perform
- high level languages like Solidity are designed to make programming easier
- when you compile a Solidity contract, compiler translates high level code into sequence of EVM opcodes
- the sequence of opcodes, known as bytecode, can then be executed on the EVM
- each opcode has an ID
- the IDs are numbers, are uint8, are represented in hex
- most common 12 below
- all opcodes
- common opcodes
add
: Arithmetic addition of the top two stack items.sub
: Arithmetic subtraction of the top two stack items.mul
: Arithmetic multiplication of the top two stack items.div
: Arithmetic division of the top two stack items.sstore
: Save a value in contract storage.sload
: Load a value from contract storage.jump
: Alter the program counter to a new location.jumpi
: Conditionally alter the program counter.push
: Place a new value on the stack.pop
: Remove an item from the stack.return
: End execution and return output data.call
: Interact with another contract. Executes a message call.delegatecall
: Executes a message call, but retains the current contract's state.staticcall
: Executes a message call but doesn't allow state changes.selfdestruct
: Destroys the contract and sends its funds to an address.keccak256
: Computes the Ethereum-SHA-3 (Keccak-256) hash of the input.sha256
: Computes the SHA-256 hash of the input.ripemd160
: Computes the RIPEMD-160 hash of the input.ecrecover
: Recovers the address associated with the public key from elliptic curve signature or return zero on error.addmod and mulmod
: Arithmetic operations with modulo.blockhash
: Provides the hash of one of the 256 most recent complete blocks.gasleft
: Returns the remaining gas.revert
: This function stops execution and reverts all state changes. It can also take a string argument, which will be used as the error message.require
: This function is similar torevert
, but it's typically used for input validation. It takes two parameters: a boolean condition, and a string message. If the condition is false, it reverts the transaction and shows the error message.assert
: This function is similar torequire
, but it's used for conditions that should never be false. Unlikerequire
,assert
consumes all remaining gas when it fails.abi.encode
,abi.encodePacked
,abi.encodeWithSelector
,abi.encodeWithSignature
: These functions encode the given arguments according to ABI.
- assembly
state vs local variables
- state
- declared outside of functions
- stored in contract storage, part of the blockchains state
- exist for the life of the contract
- local
- declared within functions
- only exist during execution of the function
- exist for life of function execution
- state variable scope
public
accessed from contract and from other contractsinternal
accessed from contract and contracts that inherit from itprivate
only access from contract itself
- state
data locations
- 3 data locations where variables can be stored
storage
- state variables
- persistant between function calls
- expensive to use
memory
- temp place to store data
- memory is erased between external function calls
- cheaper to use than storage
calldata
- special data location only for params of external contract functions
- does not persist between function calls
- read-only
send
,transfer
andcall
- 3 ways to send Ether from one account (contract) to another
- transfer
- send Ether and throws error if operation fails
- only forwards 2300 gas, limiting receivers ability to perform complex operations during the txn
- introduced as a security measure against re-entrancy attacks
- send
- similar to transfer
- but returns false instead of returning an error
- call
- most flexible and powerful
- allows you to specify the amount of gas to be forwarded along with the value being sent
- recommended way to send Ether
(bool result,) = msg.sender.call{value:_amount}("");
is sending_amount
of Ether to the msg.sender.- The function call is called on
msg.sender
, and{value:_amount}
specifies the amount of Ether to send. - The
""
is the data field, which can be used to call a function on the receiving contract. - Here it is left empty, meaning no function is called.
- The function call is called on
- more flexible but leaves open for re-enterancy attacks
don't use transfer or send after Istanbul hard fork
- hard fork introduces EIP 1884
- this increases the gas cost of the
SLOAD
operation - if the
receive
or fallback functions now use more than 2300 gas the transfer will be reverted
- Source Contract:
pragma solidity ^0.8.0;
contract SourceContract {
address payable public target;
constructor(address payable _target) {
target = _target;
}
function sendEtherUsingTransfer() public payable {
target.transfer(msg.value);
}
function sendEtherUsingSend() public payable returns (bool) {
return target.send(msg.value);
}
}
- Target Contract:
pragma solidity ^0.8.0;
contract TargetContract {
event ReceivedEther(address from, uint256 amount);
// This function is automatically called when the contract receives Ether
receive() external payable {
// This event will consume more than 2300 gas, so it will fail if called by `transfer` or `send`
emit ReceivedEther(msg.sender, msg.value);
}
// A simple function to check the contract's balance
function getBalance() public view returns (uint256) {
return address(this).balance;
}
}
In the above example:
- The
SourceContract
has methods to send Ether using bothtransfer
andsend
. - The
TargetContract
has areceive
function that gets triggered when it receives Ether. It emits an event, which will consume more than the 2300 gas stipend provided bytransfer
andsend
. Therefore, if you try to send Ether fromSourceContract
toTargetContract
, the transaction will fail because thereceive
function inTargetContract
will run out of gas.
This demonstrates the gas limitation when using transfer
and send
. If you were to remove the emit
statement from the receive
function, the transaction would succeed, as the gas consumption would be below the 2300 gas stipend.
interact with other contracts
- function calls
- simply call functions on other contracts
- use an interface
- throw exception when call fails
- low-level calls
- returns false instead of throwing an exception
- need to handle failures
call
- Ether sent with the call is provided to the target
- target executes in own context
- cannot access or modify the proxy
delegatecall
- similar to call
- target executes in the context of the proxy
- can read and write proxy state variables
staticcall
- similar to call
- readon only and cannot modify state or send Ether
callcode
- similar to delegatecall
- obsolete, discouraged
- returns false instead of throwing an exception
- function calls
contract addresses
- they are deterministic and calculated by
keccak256(sender address, nonce)
- can send Ether to a pre-determined address (which has no private key)
- then create a contract at that address that recovers the Ether
- need to get the nonce from the txn and have the sender address of course to figure out the contract address
- they are deterministic and calculated by
initialising another contract
- call
wallet = new Wallet();
- creates separate deployment of the Wallet contract, constructor is called at this point
- the msg.sender is the contract making the call
- call
contract types
- Concrete contracts
- These are complete contract definitions with all function bodies provided.
- They can be deployed as a standalone contract.
- Abstract contracts
- These contracts contain at least one function that is not fully implemented.
- They can't be deployed on their own, but other contracts can inherit from them to provide implementations for the missing functions.
- Interfaces
- These are like abstract contracts, but they cannot have any implemented functions.
- Interfaces are a way to define the minimal requirements for a contract, or to interact with contracts that are already deployed.
- Libraries
- Libraries are similar to contracts, but they cannot have state variables and cannot inherit from other contracts or be inherited from.
- Library functions can be attached to types using the
using
keyword, or can be called directly. - Libraries are often used for frequently repeated code to save gas.
- Modifiers
- Modifiers are not contract types but they are reusable pieces of code that can be included in functions to alter their behavior, often used to change the function's visibility or to include pre- and
- Concrete contracts
IERC20Metadata
- this was introduced as an optional extension to ERC20 so can't assume that tokens implement the interface
- functions in interface
- name()
- symbol()
- decimals()
structs into storage
- if you use a Struct in a function it will be assigned to storage by default, starting at 0
- it might write over some existing state variables in storage
- example
ERC-20
- alice approves bob to transfer 10 tokens
- alice then approves bob to transfer 20 tokens
- bob frontruns the second call and transfers 10 tokens
- then backruns the second call and transfers 20 tokens
- write up
- Reentrant Calls: Tokens allowing reentrant calls on transfer, exploited in past incidents.
- Missing Return Values: Tokens not returning a bool for ERC20 methods, causing issues like stuck tokens.
- Fee on Transfer: Tokens taking a transfer fee or potentially implementing fees in the future.
- Balance Modifications Outside of Transfers: Tokens making arbitrary balance changes, affecting contract operations with outdated information.
- Upgradable Tokens: Tokens that can be upgraded, altering their logic and affecting dependent contracts.
- Flash Mintable Tokens: Tokens allowing temporary minting for one transaction, posing supply risks.
- Tokens with Blocklists: Tokens with admin-controlled address blocklists, restricting transfers.
- Pausable Tokens: Tokens that can be paused by an admin, exposing users to risks.
- Approval Race Protections: Tokens disallowing approval changes without zeroing out existing approvals first.
- Revert on Approval To Zero Address: Tokens reverting when trying to approve the zero address for spending.
- Revert on Zero Value Approvals: Tokens that revert on approving a zero value amount.
- Revert on Zero Value Transfers: Tokens reverting when transferring a zero value amount.
- Multiple Token Addresses: Issues arising from tokens with multiple addresses, like fund mismanagement.
- Low Decimals: Tokens with low decimal points, leading to precision loss.
- High Decimals: Tokens with more than 18 decimals, risking overflow reverts.
transferFrom
withsrc == msg.sender
: Variations in token behavior whentransferFrom
is called with the sender as the caller.- Non
string
Metadata: Tokens with metadata fields not following ERC20 specifications. - Revert on Transfer to the Zero Address: Tokens reverting on attempts to transfer to
address(0)
. - No Revert on Failure: Tokens not reverting on failure, but returning
false
. - Revert on Large Approvals & Transfers: Tokens reverting if approval or transfer values exceed a certain limit.
- Code Injection Via Token Name: Risk of malicious code in token
name
attributes. - Unusual Permit Function: Tokens with
permit()
implementations deviating from standard specifications. - Transfer of less than
amount
: Tokens transferring only the user's balance in specific cases, affecting systems that rely on transferred amounts.
13 Compiler Bugs
- description
- Solidity or Vyper compiler can lead to vulnerable bytecode when compiling high level language to opcodes
You're correct in noting that both Vyper and Solidity ultimately compile down to EVM (Ethereum Virtual Machine) bytecode, which consists of opcodes that the EVM understands and executes. The opcodes themselves are standardized and are part of the EVM specification. Therefore, the opcodes themselves are not inherently buggy.
However, the potential for bugs arises in how these opcodes are arranged and sequenced by the compiler. Here's a breakdown of where issues might arise:
Order and Sequence of Opcodes: The way opcodes are sequenced can lead to different behaviors. A compiler might produce a sequence of opcodes that doesn't correctly implement the intended high-level logic. This is where compiler bugs can be especially dangerous, as they can introduce unintended behaviors in the compiled code.
Optimization Errors: Compilers often have optimization phases that aim to make the resulting bytecode more efficient in terms of gas usage or execution speed. Errors in these optimization processes can introduce vulnerabilities.
High-Level Language Features: Features in high-level languages like Vyper or Solidity might have specific implementations that, when compiled, introduce unexpected behaviors or vulnerabilities. This is especially true for newer or less-tested features.
Ambiguities in the Language Specification: If the high-level language's specification is ambiguous or lacks clarity in certain areas, different compilers (or versions of the same compiler) might interpret and compile code differently, leading to inconsistencies and potential vulnerabilities.
External Calls and Interactions: How the compiled code interacts with other contracts or external functions can also be a source of vulnerabilities. The way these interactions are compiled into opcodes can vary based on the high-level language and compiler.
In the context of the article you shared, if there's a known bug in the Vyper compiler's output (i.e., the sequence or arrangement of opcodes), then scanning the blockchain for contracts that contain that specific opcode pattern can help identify potentially vulnerable contracts. This doesn't mean the opcodes themselves are faulty, but rather the specific arrangement or sequence produced by the compiler is.
14. DoS
Gas Limit
Unbounded Operations
Reverting Transactions: When a transaction hits the gas limit, it reverts, meaning all changes it made are undone. However, the gas used up to that point is still consumed, and the sender pays for it.
Funds Getting Stuck: If a contract is designed such that it tries to perform an operation (like sending funds to a list of recipients) and consistently exceeds the gas limit, then the operation can never be completed successfully. This could lead to funds being effectively "stuck" in the contract if there's no alternative way to retrieve or send them.
Bad Actor Blocking the Contract: Consider a contract that allows users to add themselves to a list and then has a function to process something for every user on that list. A bad actor could add a large number of addresses to this list, knowing that the processing function will always exceed the gas limit due to the sheer number of entries. This means the function can never be executed successfully, effectively "blocking" that functionality of the contract. If this function is crucial for the contract's operation, it can render the contract useless.
Mitigation: To prevent such issues, contracts should avoid unbounded loops or operations that could grow indefinitely. Instead, they can use patterns like the one in the provided code, which processes operations in manageable chunks, ensuring they don't hit the gas limit. Another approach, as mentioned, is the "pull payment" system, where individual users initiate their own transactions, spreading out the gas usage across multiple transactions.
Block Stuffing
- transaction spamming
- attacker fills blocks by submitting txns with high gas fees which are attractive to miners
- it is expensive to do this
- Ethereum block gas limit is ~15 million gas
- if there is enough of an incentive it can happen
- block stuffing attack on gambling Dapp Fomo3D
- The app had a countdown timer, and users could win a jackpot by being the last to purchase a key, except everytime 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.
- block stuffing attack on gambling Dapp Fomo3D
Unexpected Revert
- DoS may be caused when logic is unable to be executed as a result of an unexpected revert
- could revert for a number of reasons, need to consider all ways logic may revert
Reverting Funds Transfer
- try and send funds to a user and the functionality relies on that fund transfer going through
- an issue in the case that funds are sent to a smart contract and that contract can create a fallback function that reverts all payments
// INSECURE
contract Auction {
address currentLeader;
uint highestBid;
function bid() payable {
require(msg.value > highestBid);
require(currentLeader.send(highestBid)); // Refund the old leader, if it fails then revert
currentLeader = msg.sender;
highestBid = msg.value;
}
}
tries to refund the old highest bidder but if that action always reverts the function can never set a new highest bidder
also an issue when looping through array making payments to addresses
- if one payment fails the function is reverted and nobody is paid
address[] private refundAddresses;
mapping (address => uint) public refunds;
// bad
function refundAll() public {
for(uint x; x < refundAddresses.length; x++) { // arbitrary length iteration based on how many addresses participated
require(refundAddresses[x].send(refunds[refundAddresses[x]])) // doubly bad, now a single failure on send will hold up all funds
}
}
- solve by switching to a pull payment system instead of push payment system above
- seperate each payment into it's own transaction and have the recipient call the function
contract auction {
address highestBidder;
uint highestBid;
mapping(address => uint) refunds;
function bid() payable external {
require(msg.value >= highestBid);
if (highestBidder != address(0)) {
refunds[highestBidder] += highestBid; // record the refund that this user can claim
}
highestBidder = msg.sender;
highestBid = msg.value;
}
function withdrawRefund() external {
uint refund = refunds[msg.sender];
refunds[msg.sender] = 0;
(bool success, ) = msg.sender.call.value(refund)("");
require(success);
}
}
Over / Underflow
- when math is checked, the effect of an over/under flow is a revert which might DoS important logic
- important to ensure that any valid input will not result in an over/underflow
- take extra care working with smaller ints e.g: int8, uint8, int16, uint16, int24, uint24
Unexpected Balance
- careful enforcing expected contract balances of tokens or Ether because those balances could be increated by an attacker looking to cause a revert
- e.g: a contract which expects Eth balance to be 0 for the first deposit, attacker forcibly sends Ether before the first deposit causing all deposits to revert
Endless Loop
A potential pitfall in Ethereum smart contract development related to loops, gas optimization, and the use of the continue
statement.
Gas Optimization: In Ethereum, every operation costs gas. Developers often try to optimize their contracts to use as little gas as possible. One way to do this is by minimizing the number of operations in loops.
Unchecked Field: If a loop uses an index variable (like
i
), and this variable is incremented or modified inside the loop without proper checks, it can lead to unexpected behavior.Continue Statement: The
continue
statement in programming skips the current iteration of a loop and moves to the next one. If used in a loop where the index variable is incremented after thecontinue
statement, it can cause the loop to repeatedly process the same index.Out Of Gas (OOG) Error: If the loop keeps processing the same index due to the misuse of the
continue
statement, it can run indefinitely until the transaction runs out of gas, resulting in an OOG error.
Example:
Consider a simple Solidity function that processes an array of integers:
pragma solidity ^0.8.0;
contract LoopExample {
uint[] public numbers;
function processNumbers() public {
for (uint i = 0; i < numbers.length; ) {
if (numbers[i] == 42) {
// Skip this number
continue;
}
// Some processing logic here...
i++; // Incrementing the index here
}
}
}
In the above contract:
- We have a loop that processes each number in the
numbers
array. - If a number is
42
, the loop should skip it using thecontinue
statement. - The index
i
is incremented at the end of the loop.
The problem here is that if numbers[i]
is 42
, the continue
statement will skip the rest of the loop, including the i++
statement. This means the loop will keep processing the same number (at index i
) over and over, leading to an infinite loop and eventually an OOG error.
Solution:
To fix this, the increment operation should be placed before the continue
statement:
if (numbers[i] == 42) {
i++; // Increment the index before continuing
continue;
}
This ensures that the loop index is always incremented, preventing infinite loops and OOG errors.
Griefing
- usually related to business logic
- attacks cause a negative impact on operation of smart contract despite no direct profit for attacker
- can disrupt overal systems or just at critical moments
e.g:
- contract which only allows funds to be witdrawn 24hrs after last deposit
- griefing attack to keep depositing a small amount every 23hrs, prevents funds withdrawal
Gas Griefing
- subset of griefing attacks
- affects smart contracts performing external calls without checking the return value from the external call
- due to 63/64 rule which determines how much gas can be forwarded to the external call
e.g:
contract A function makes an external call to contract B function
txn has enough gas to complete contract A function execution, but not enough for the external call
- 63/64 rule means that contract A can only pass 63/64ths of the gas onto contract B and keep 1/64 in reserve to complete execution of contract A
contract B function reverts, contract A does not check for the revert, contract A function completes and the txn is sent to blockchain
need to check that contract B call completed and if not, but it should, then revert from contract A so that the txn is not processed
the griefing impact comes from contract A completing under unanticipated circumstances e.g: contract B has not completed, and it might grief contract A if in this case it puts it in some strange state etc.
Reset Cooldown
- video
- user can only withdraw after a cooldown period
- timestamp recorded for user
- timestamp + time = cooldown
- after cooldown user can withdraw
- function which will clear the cooldown
- it clears the cooldown timestamp from the array for this user
- which effectively means the user cannot pass the cooldown period and they cannot withdraw
- other users can call this and continually block another user from withdrawal
15. Partial Payments, Repayments and Dust Payments
Partial Payments:
Is moving money partially possible?
- This checks if the contract allows for only a portion of the total funds or assets to be moved or utilized.
- Examples include:
- Moving liquidity: Can a user withdraw only a part of their liquidity from a pool?
- Repaying: Can a user repay only a portion of a loan?
- Borrowing: Can a user borrow less than their maximum allowable limit?
- Transferring: Can funds be transferred in partial amounts?
If payment is greater than required, does it send back the surplus?
- This ensures that if a user overpays (sends more funds than necessary), the contract automatically refunds the excess amount.
Are the fees applied only to the partial amount?
- If only a partial amount is being transacted, this checks if fees are correctly applied only on that partial amount and not on the total balance or some other amount.
If there is any type of money flow that could be partial, is there a way the funds could be locked due to a state mismatch?
- This is a crucial security check. It ensures that partial transactions don't result in funds getting stuck due to inconsistencies in the contract's state.
Can the other part of the payment be claimed later?
- If a transaction is partial, this checks if the remaining amount can be claimed or transacted at a later time without issues.
Repayments:
Is third-party repaying possible?
- This checks if someone other than the borrower can repay the loan on their behalf.
What if a user doesn't want somebody else to repay the loan?
- This is a privacy and security concern. It ensures that users have control over who can repay their loans, preventing unwanted repayments.
If there's a functionality to pause, does the contract still charge interest/fees?
- Some contracts have a "pause" functionality for emergencies. This point checks if interest or fees are still accrued during this paused state.
Can the user still pay back the loan even though the contract is paused?
- This ensures that users can still settle their debts even if the contract's other functionalities are paused. It's crucial for user funds' accessibility.
Can loans become insolvent and be liquidated while repayments are paused?
- This question addresses a potential risk where, if repayments on a loan are paused (perhaps due to a contract's emergency mechanism), the loan's collateral value might drop below the required threshold, making the loan insolvent. If this happens, the loan might be subject to liquidation, where the collateral is sold off to repay the loan.
If loans became insolvent while repayments were paused, what happens when repayments are unpaused - do they get immediately liquidated by bots?
- This is a follow-up to the previous point. It's asking about the scenario where a loan becomes insolvent during a pause. Once the pause is lifted, there's a concern that automated bots (which monitor the blockchain for profitable opportunities) might immediately trigger the liquidation of these insolvent loans, potentially at the detriment of the borrower.
If the protocol has a token whitelist and if a token is removed from that whitelist, do existing loans using that token become impossible to repay?
- Many DeFi platforms have whitelists of approved tokens that can be used as collateral for loans. This question raises a concern about what happens if a token, which is already being used as collateral for existing loans, gets removed from this whitelist. Specifically, it asks if borrowers would then be unable to repay or manage their loans due to the token's removal.
If repaying multiple loans in one transaction is possible, does the excess from the first repayment roll over & repay the next loan?
- This question addresses a scenario where a user might have multiple loans and wants to repay them in a single transaction. It asks if any surplus amount from repaying one loan (maybe the user repaid more than the outstanding amount) would automatically be applied to the next loan in the queue.
Dusting Attacks
A "dusting attack" is a type of malicious activity related to cryptocurrencies, especially prominent in the context of Bitcoin and other blockchain platforms. The term "dust" in cryptocurrency refers to a very small amount of tokens or coins, often so tiny that many users ignore them.
Attack Mechanism: In a dusting attack, the attacker sends a small amount of cryptocurrency (the "dust") to a large number of addresses.
Purpose: The primary goal of a dusting attack is not to steal funds but rather to compromise the privacy of the users of those addresses. By analyzing the blockchain data, attackers can link those dusted addresses to other addresses and identify patterns, potentially revealing the identity of the person or organization behind each address.
Consequences: Once the attacker identifies the owners of the addresses, they can use this information for various malicious purposes, such as phishing attacks, ransom demands, or other forms of cyber extortion.
Mechanics
Initial State: You have an address "A" with 1 ETH. An attacker sends a small "dust" amount, say 0.0001 ETH, to your address.
Consolidation: Later, you decide to transfer your entire balance from address "A" to another address "B". This means you're sending 1.0001 ETH in a single transaction.
Analysis by Attacker: The attacker, who has been monitoring the blockchain for transactions involving the dust amount (0.0001 ETH), notices this transaction. Since the dust was consolidated with the 1 ETH and sent to "B", the attacker can infer that addresses "A" and "B" are likely controlled by the same entity (you).
Further Implications: If, for any reason, address "A" had been previously linked to your real-world identity (maybe through a KYC process on an exchange, a public donation, or any other means), then address "B" could potentially be linked to your identity as well, even if "B" had never been used in any identifiable way before.
This is the essence of how a dusting attack can compromise privacy. By observing how dust is used in subsequent transactions, attackers can make educated guesses about address ownership and control.
Relation to the Partial and Repayments
Partial Payments: If a smart contract allows for partial payments, it might be susceptible to receiving "dust" amounts. If these small transactions are not handled correctly, they could clutter the contract, lead to incorrect fee calculations, or even be used to trigger unexpected contract behaviors.
Repayments: If third-party repayments are possible, an attacker could use dusting as a method to send small repayments to a contract on behalf of a user. This could be used to track user behaviors or exploit potential vulnerabilities in the repayment logic.
State Mismatches: Dusting attacks could potentially lead to state mismatches in a contract, especially if the contract isn't designed to handle a large number of small transactions. This could result in funds getting locked or other unintended behaviors.
In the context of the checklist and smart contract auditing, it's essential to ensure that the contract can handle receiving small amounts without adverse effects and that it doesn't inadvertently reveal user behaviors or patterns that could compromise user privacy.
Collateral
- video
- when you have a bunch of different coins in the collateral for a loan
- each coin is different risk profile
- each coin you can borrow a different amount on
- 1 USDT you can borrow 80c
- 1 BTC you can borrow 70c
- 1 SOL you can borrow 50c
- this is to account for fluctuations in the value of the collateral
- the loan should remain overcollateralised
- more value in collateral than in loan
- when liquidation occurs the invariant is
- the collateral should remain high enough to secure the loan despite market fluctuations
- if the system liquidates and takes USDT
- then the overall risk of collateral goes up
- does the system account for that?
- does the lending position remain the same or better after partial liquidation
16. Gas
- description
- high use of Gas when not necessary
- usually an info or minor issue
- prevention
- refactoring
Notes
- iterating over a
for
loop - if checking array.length in the for loop it will cost gas to look that up every time
- cache the array length outside of the loop instead
for (uint256 _i; _i < _safes.length; _i++) {
//fix
uint256 length = _safes.length;
for (uint256 _i; _i < length; _i++) {
- checking for overflow and underflow unnecessarily
- in Solidity 0.8.x arithmetic operations revert on overflow and underflow
- it's an automatic check but also incurs a gas cost
- if you are certain that an operation will not overflow or underflow can use
unchecked
block to skip the checks and save on gas
uint256 x = 10;
uint256 y = 20;
uint256 result = x + y;
//fix
uint256 x = 10;
uint256 y = 20;
uint256 result;
unchecked {
result = x + y;
}