✅
Web3 Security Research Trivia
- ABI Encoding behaves differently in memory and calldata
- ecrecover needs zero address check because of geth implementation instead of the precompile
- Large uint casts to int -> overflow
- Solidity's return is different from Yul's return
- bytes is different from bytes32
- return; is equivalent to STOP opcode
- create2 precomputing address should consider constructor's input parameters
- Huff __FUNC_SIG() is done during the compilation phase
- msg.sender in forge script
- SELFDESTRUCT deletes bytecode at the very end of tx
- bytes32 pads 0's on the right
- UDMV
- vm.startPrank() can set both msg.sender and tx.origin
- One way to silence linter on unused variable
Dynamic types are encoded as 2-part in memory and 3-part in calldata. The extra field in the calldata case is the "offset". This field is needed because data chunks are not stored consecutively in calldata. The actual content of a dynamic types are stored behind static types.
For uint256 in the range
2**255
to 2**256 - 1
, casting it to int256 causes overflow. The result is a negative number. This is because the max int256 is 2**255 - 1
and max uint256 is 2**256 - 1
.// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Test{
function overflow() public pure returns (int256) {
uint256 temp = 2**255 + 10000000;
return int256(temp);
}
}

When writing function selector we should always change
uint
to uint256
, but be aware that don't do the same for bytes
. bytes
is dynamic length and bytes32
is fixed length, they are absolutely different.Even if the function does not have a return value, you can write
return;
to stop the execution of a function's logic. I tested in Remix debugger and I found that return;
is just a STOP opcode.I found this in a Remix debugging session: __FUNC_SIG() actually stores the function selector into contract bytecode during compilation phase. In contrast, encodeWithSignature() in Solidity does heavy computation at runtime, so Huff is a lot more efficient.
Without using
vm.startBroadcast()
, msg.sender is just address(this).If
vm.startBroadcast()
is used, msg.sender is the the EOA address corresponding to the private key you provide via command line argument.Another case:
uint256 attackerPrivateKey = vm.envUint("PRIVATE_KEY");
address attackerAddress = <your_EOA_address>;
vm.deal(attackerAddress, 1 ether);
vm.startBroadcast(attackerPrivateKey);
In this case msg.sender is just attackerAddress.


Code:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract x60 {
function notGood() external pure returns(bytes memory) {
bytes memory data;
assembly {
mstore(0x60, 0x20)
}
return data;
}
function shouldBeGood() external pure returns(bytes memory) {
bytes memory data = new bytes(0);
assembly {
mstore(0x60, 32)
}
return data;
}
}


This seizeTokens is unused in this function, so
seizeTokens;
can be used to silence linter such as Solhint.Last modified 6d ago