ctfnote.com
Search
K

Web3 Security Research Trivia

Table of contents

  • 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

ABI Encoding behaves differently in memory and calldata

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.

ecrecover needs zero address check because of geth implementation instead of the precompile

Large uint casts to int -> overflow

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);
}
}

Solidity' return is different from Yul's return

bytes is different from bytes32

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.

return; is equivalent to STOP opcode

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.

create2 precomputing address should consider constructor's input parameters

Huff __FUNC_SIG() is done during the compilation phase

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.

msg.sender in forge script

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.

SELFDESTRUCT deletes bytecode at the very end of tx

bytes32 pads 0's on the right

UDMV

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;
}
}

vm.startPrank() can set both msg.sender and tx.origin

One way to silence linter on unused variable

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