Web3 Security Research Trivia

Table of contents

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.

https://twitter.com/ret2basic/status/1678633214837923840

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

https://twitter.com/lovethewired/status/1681333654209941505

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

https://twitter.com/ret2basic/status/1699768097270563177

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

https://twitter.com/ret2basic/status/1700449951090909405

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

https://twitter.com/ret2basic/status/1703261157652681181

bytes32 pads 0's on the right

UDMV

https://twitter.com/AmadiMichaels/status/1694815830465302882

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.

msg.sender when calling external function using this keyword

When calling an external function within the same contract internally, you would use the this keyword, such as this.functionCall() where functionCall() is an external function written in the same contract. The question is, who is msg.sender when using the this keyword?

The answer is msg.sender = address(this). For a test in Remix IDE:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SenderPreservationExample {
    address public lastDirectCaller;
    address public lastInternalCaller;

    function setCallerDirect() public {
        lastDirectCaller = msg.sender;
    }

    function setCallerInternal() public {
        lastInternalCaller = msg.sender;
    }

    function callInternalViaThis() public {
        this.setCallerInternal();
    }

    function getCallers() public view returns (address, address) {
        return (lastDirectCaller, lastInternalCaller);
    }
}

Call callInternalViaThis() with any EOA address, you will see that lastInternalCaller is set to contract address not the original EOA address.

Last updated