# Web3 Security Notes

## Table of contents

* [x] ABI Encoding behaves differently in memory and calldata
* [x] ecrecover needs zero address check because of geth implementation instead of the precompile
* [x] Large uint casts to int -> overflow
* [x] Solidity's return is different from Yul's return
* [x] bytes is different from bytes32
* [x] return; is equivalent to STOP opcode
* [x] create2 precomputing address should consider constructor's input parameters
* [x] Huff \_\_FUNC\_SIG() is done during the compilation phase
* [x] msg.sender in forge script
* [x] SELFDESTRUCT deletes bytecode at the very end of tx
* [x] bytes32 pads 0's on the right
* [x] UDMV
* [x] vm.startPrank() can set both msg.sender and tx.origin
* [x] One way to silence linter on unused variable
* [x] msg.sender when calling external function using `this` keyword
* [x] selfdestruct after Pectra update -> EIP-6780

## :white\_check\_mark: 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>

## :white\_check\_mark: ecrecover needs zero address check because of geth implementation instead of the precompile

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

## :white\_check\_mark: 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`.

```solidity
// 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);
    }
}
```

<figure><img src="/files/blg31IMKaqheQaLzKPPD" alt=""><figcaption></figcaption></figure>

## :white\_check\_mark: Solidity' return is different from Yul's return

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

## :white\_check\_mark: 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.

## :white\_check\_mark: 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.

## :white\_check\_mark: create2 precomputing address should consider constructor's input parameters

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

## :white\_check\_mark: 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.

## :white\_check\_mark: 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:

```solidity
        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.

## :white\_check\_mark: SELFDESTRUCT deletes bytecode at the very end of tx

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

## :white\_check\_mark: bytes32 pads 0's on the right

<figure><img src="/files/nHE8CvbwbrFxEx4ET8Yz" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/L2gf7hFuIKAwUmRQAQr6" alt=""><figcaption></figcaption></figure>

## :white\_check\_mark: UDMV

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

Code:

<pre class="language-solidity"><code class="lang-solidity"><strong>// SPDX-License-Identifier: MIT
</strong>pragma solidity ^0.8.20;
<strong>
</strong>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;
	}
}
</code></pre>

## :white\_check\_mark: vm.startPrank() can set both msg.sender and tx.origin

<figure><img src="/files/grQgMHQD3LGj0Oc86RJx" alt=""><figcaption></figcaption></figure>

{% embed url="<https://book.getfoundry.sh/cheatcodes/start-prank>" %}

## :white\_check\_mark: One way to silence linter on unused variable

<figure><img src="/files/MwjutOP1r5rYRkTc8jCS" alt=""><figcaption></figcaption></figure>

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

## :white\_check\_mark: 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:

```solidity
// 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.

### :white\_check\_mark: selfdestruct after Pectra update -> EIP-6780

[EIP-6780](https://eips.ethereum.org/EIPS/eip-6780) says:

> The new functionality (of selfdestruct opcode) will be only to send all Ether in the account to the target, except that the current behaviour is preserved when `SELFDESTRUCT` is called in the same transaction a contract was created.

This can be proven by a series of tests: <https://gist.github.com/ret2basic/9464cd936e020aa2ba1c0b6f8f2f800a>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://ret2basic.gitbook.io/ctfwriteup/web3-ctf/web3-security-notes.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
