@@ -1735,3 +1735,164 @@ def test_extcodehash_subcall_create2_oog(
17351735 )
17361736
17371737 state_test (pre = pre , post = post , tx = tx )
1738+
1739+
1740+ @pytest .mark .ported_from (
1741+ [
1742+ "https://github.com/ethereum/tests/blob/v13.3/src/GeneralStateTestsFiller/stExtCodeHash/codeCopyZero_ParisFiller.yml" , # noqa: E501
1743+ ],
1744+ )
1745+ @pytest .mark .parametrize (
1746+ "target_type" ,
1747+ [
1748+ "nonexistent" ,
1749+ "existing" ,
1750+ ],
1751+ )
1752+ def test_extcodecopy_zero_code (
1753+ state_test : StateTestFiller ,
1754+ pre : Alloc ,
1755+ target_type : str ,
1756+ ) -> None :
1757+ """
1758+ Test EXTCODECOPY/EXTCODESIZE/EXTCODEHASH of accounts with no code.
1759+
1760+ Two account types: nonexistent (no state at all) and existing
1761+ (EOA with balance, no code). EXTCODECOPY writes nothing to memory
1762+ (stays zero). EXTCODEHASH is zero for nonexistent, keccak256("")
1763+ for existing accounts.
1764+
1765+ TODO: The original test also intended to cover empty accounts
1766+ (zero nonce, zero balance, no code), but such accounts cannot
1767+ exist in post-Paris forks due to EIP-161 cleanup.
1768+ """
1769+ storage = Storage ()
1770+
1771+ if target_type == "nonexistent" :
1772+ target = pre .nonexistent_account ()
1773+ expected_hash : int | bytes = 0
1774+ else : # existing
1775+ target = pre .fund_eoa (amount = 1 )
1776+ expected_hash = keccak256 (b"" )
1777+
1778+ code = (
1779+ Op .EXTCODECOPY (target , 0 , 0 , 32 )
1780+ + Op .SSTORE (storage .store_next (0 , "extcodecopy" ), Op .MLOAD (0 ))
1781+ + Op .SSTORE (
1782+ storage .store_next (0 , "extcodesize" ),
1783+ Op .EXTCODESIZE (target ),
1784+ )
1785+ + Op .SSTORE (
1786+ storage .store_next (expected_hash , "extcodehash" ),
1787+ Op .EXTCODEHASH (target ),
1788+ )
1789+ + Op .SSTORE (
1790+ storage .store_next (1 , "callcode_result" ),
1791+ Op .CALLCODE (50_000 , target , 0 , 0 , 0 , 0 , 0 ),
1792+ )
1793+ )
1794+
1795+ code_address = pre .deploy_contract (code , storage = storage .canary ())
1796+
1797+ tx = Transaction (
1798+ sender = pre .fund_eoa (),
1799+ to = code_address ,
1800+ gas_limit = 400_000 ,
1801+ )
1802+
1803+ state_test (
1804+ pre = pre ,
1805+ post = {code_address : Account (storage = storage )},
1806+ tx = tx ,
1807+ )
1808+
1809+
1810+ @pytest .mark .ported_from (
1811+ [
1812+ "https://github.com/ethereum/tests/blob/v13.3/src/GeneralStateTestsFiller/stExtCodeHash/codeCopyZero_ParisFiller.yml" , # noqa: E501
1813+ ],
1814+ )
1815+ def test_codecopy_zero_in_create2 (
1816+ state_test : StateTestFiller ,
1817+ pre : Alloc ,
1818+ ) -> None :
1819+ """
1820+ Test CODECOPY inside CREATE2 initcode that deploys empty code.
1821+
1822+ The initcode does CODECOPY(0,0,32) which copies the first 32 bytes
1823+ of the initcode itself (not the deployed code). It then checks
1824+ EXTCODESIZE(ADDRESS) and EXTCODEHASH(ADDRESS) of self during init,
1825+ which see the account as having empty code. The deployed contract
1826+ retains the storage set during init.
1827+ """
1828+ storage = Storage ()
1829+
1830+ # Build the initcode that queries itself and deploys empty code.
1831+ # During init: CODECOPY copies the initcode, EXTCODESIZE(self)=0,
1832+ # EXTCODEHASH(self)=keccak256("").
1833+ initcode = (
1834+ Op .CODECOPY (0 , 0 , 32 )
1835+ + Op .SSTORE (0x50 , Op .MLOAD (0 ))
1836+ + Op .SSTORE (0x51 , Op .EXTCODESIZE (Op .ADDRESS ))
1837+ + Op .SSTORE (0x52 , Op .EXTCODEHASH (Op .ADDRESS ))
1838+ + Op .SSTORE (
1839+ 0x53 ,
1840+ Op .EXTCODESIZE (Op .CALLCODE (50_000 , Op .ADDRESS , 0 , 0 , 0 , 0 , 0 )),
1841+ )
1842+ + Op .EXTCODECOPY (Op .ADDRESS , 0 , 0 , 32 )
1843+ + Op .SSTORE (0x54 , Op .MLOAD (0 ))
1844+ # Return empty code (size 0).
1845+ + Op .RETURN (0 , 0 )
1846+ )
1847+
1848+ # Factory: CREATE2 and return the created address.
1849+ factory_code = (
1850+ Om .MSTORE (initcode , 0 )
1851+ + Op .MSTORE (
1852+ 0 ,
1853+ Op .CREATE2 (value = 0 , offset = 0 , size = len (initcode ), salt = 0 ),
1854+ )
1855+ + Op .RETURN (0 , 32 )
1856+ )
1857+
1858+ factory = pre .deploy_contract (factory_code , balance = 10 ** 18 )
1859+
1860+ # Caller: invoke factory and store created address.
1861+ caller_code = Op .CALL (550_000 , factory , 0 , 0 , 0 , 0 , 32 ) + Op .SSTORE (
1862+ storage .store_next (0 , "created_address" ), Op .MLOAD (0 )
1863+ )
1864+
1865+ caller = pre .deploy_contract (caller_code , storage = storage .canary ())
1866+
1867+ created = compute_create2_address (
1868+ address = factory , salt = 0 , initcode = initcode
1869+ )
1870+ storage [0 ] = created
1871+
1872+ # First 32 bytes of initcode — what CODECOPY(0,0,32) returns.
1873+ initcode_word0 = bytes (initcode )[:32 ]
1874+
1875+ tx = Transaction (
1876+ sender = pre .fund_eoa (),
1877+ to = caller ,
1878+ gas_limit = 1_400_000 ,
1879+ )
1880+
1881+ state_test (
1882+ pre = pre ,
1883+ post = {
1884+ caller : Account (storage = storage ),
1885+ created : Account (
1886+ nonce = 1 ,
1887+ code = b"" ,
1888+ storage = {
1889+ 0x50 : initcode_word0 ,
1890+ 0x51 : 0 ,
1891+ 0x52 : keccak256 (b"" ),
1892+ 0x53 : 0 ,
1893+ 0x54 : 0 ,
1894+ },
1895+ ),
1896+ },
1897+ tx = tx ,
1898+ )
0 commit comments