diff --git a/foundry.toml b/foundry.toml index b6d233b0..86ffdfda 100644 --- a/foundry.toml +++ b/foundry.toml @@ -22,6 +22,23 @@ cache_path = 'forge-cache/yul' remappings = [] auto_detect_remappings = false +[profile.gas-dimensions] +src = 'gas-dimensions/src' +test = 'gas-dimensions/test' +script = 'gas-dimeinsions/script' +out = 'out/gas-dimensions' +libs = ['lib'] +cache_path = 'forge-cache/gas-dimensions' +optimizer = false +yul_optimizer = false +via_ir = false +evm_version = 'cancun' +remappings = ['ds-test/=lib/forge-std/lib/ds-test/src/', + 'forge-std/=lib/forge-std/src/'] +fs_permissions = [{ access = "read", path = "./"}] +include_paths = ['gas-dimensions/src/', 'gas-dimensions/scripts'] +auto_detect_remappings = false + [fmt] line_length = 100 tab_width = 4 diff --git a/gas-dimensions/README.md b/gas-dimensions/README.md new file mode 100644 index 00000000..fbff1bb4 --- /dev/null +++ b/gas-dimensions/README.md @@ -0,0 +1,13 @@ +# Gas Dimension Test Contracts + +This folder contains smart contracts that force various types of opcodes to run in different configurations. This is used to test gas accounting across multiple gas dimensions, e.g. computation, state read/write, storage growth, etc. + +# Scripts for Debugging + +The scripts for running the contracts are intended to be used with forge script, e.g. + +```bash +forge script gas-dimensions/scripts/Sstore.s.sol -vvvvv --private-key "nitro.dev.node.private.key.here" --slow --broadcast --rpc-url http://127.0.0.1:8547 --priority-gas-price "1000000000" --with-gas-price "2000000000" -g "10000" --chain-id "412346" +``` + +These scripts can be helpful when manually debugging the code in `gas-dimensions/src/` against a local dev node. diff --git a/gas-dimensions/scripts/Balance.s.sol b/gas-dimensions/scripts/Balance.s.sol new file mode 100644 index 00000000..d561ddac --- /dev/null +++ b/gas-dimensions/scripts/Balance.s.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Script, VmSafe, console} from "forge-std/Script.sol"; +import {Balance} from "../src//Balance.sol"; + +contract BalanceScript is Script { + function setUp() public {} + + function run() public { + vm.startBroadcast(address(0x3f1Eae7D46d88F08fc2F8ed27FCb2AB183EB2d0E)); + payable(address(0xdeadbeef)).transfer(1000000000000000000); + Balance balance = new Balance(); + balance.callBalanceCold(); + balance.callBalanceWarm(); + vm.stopBroadcast(); + } +} diff --git a/gas-dimensions/scripts/Call.s.sol b/gas-dimensions/scripts/Call.s.sol new file mode 100644 index 00000000..38532195 --- /dev/null +++ b/gas-dimensions/scripts/Call.s.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Script, VmSafe, console} from "forge-std/Script.sol"; +import {Caller, Callee} from "../src/Call.sol"; + +contract CallTestScript is Script { + function setUp() public {} + + function run() public { + vm.startBroadcast(address(0x3f1Eae7D46d88F08fc2F8ed27FCb2AB183EB2d0E)); + + Callee callee1 = new Callee(); + Callee callee2 = new Callee(); + Callee callee3 = new Callee(); + Callee callee4 = new Callee(); + Caller caller = new Caller(); + payable(caller).transfer(1 ether); + + // five axes: + // warm / cold + // target has code / target has no code + // target funded / not funded + // sending money with the call / no transfer + // memory expansion / memory unchanged + + // 1. warm + target has code + target not funded + zero value + memory unchanged + caller.warmNoTransferMemUnchanged(address(callee1)); + // 2. warm + target has code + target not funded + positive value + memory unchanged + caller.warmPayableMemUnchanged(address(callee2)); + // 3. warm + target has no code + zero value + caller.warmNoTransferMemExpansion(address(0xbeef)); + // 4. warm + target has no code + positive value + caller.warmPayableMemExpansion(address(0xbeef)); + // 5. cold + target has code + zero value + caller.coldNoTransferMemUnchanged(address(callee3)); + // 6. cold + target has code + positive value + caller.coldPayableMemUnchanged(address(callee4)); + // 7. cold + target has no code + zero value + caller.coldNoTransferMemExpansion(address(0xbeef)); + // 8. cold + target has no code + positive value + caller.coldPayableMemExpansion(address(0xbeef)); + + vm.stopBroadcast(); + } +} diff --git a/gas-dimensions/scripts/Counter.s.sol b/gas-dimensions/scripts/Counter.s.sol new file mode 100644 index 00000000..82809f0d --- /dev/null +++ b/gas-dimensions/scripts/Counter.s.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Script, VmSafe, console} from "forge-std/Script.sol"; +import {Counter} from "../src/Counter.sol"; +import {Cloner} from "../src/Cloner.sol"; + +contract CounterScript is Script { + function setUp() public {} + + function run() public { + vm.startBroadcast(address(0x3f1Eae7D46d88F08fc2F8ed27FCb2AB183EB2d0E)); + Counter counterImpl = new Counter(); + console.log("counterImpl", address(counterImpl)); + Cloner cloner = new Cloner(address(counterImpl)); + console.log("cloner", address(cloner)); + + address counter1Addr = cloner.createCounter(); + address counter2Addr = cloner.create2Counter(bytes32(uint256(1))); + + console.log("counter1 addr", counter1Addr); + console.log("counter2 addr", counter2Addr); + + Counter counter1 = Counter(counter1Addr); + counter1.setNumber(1); + console.log("counter1 set number", counter1.number(), " on ", counter1Addr); + counter1.increment(); + console.log("counter1 incremented", counter1.number(), " on ", counter1Addr); + vm.stopBroadcast(); + } +} diff --git a/gas-dimensions/scripts/Create.s.sol b/gas-dimensions/scripts/Create.s.sol new file mode 100644 index 00000000..3de967bf --- /dev/null +++ b/gas-dimensions/scripts/Create.s.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Script, VmSafe, console} from "forge-std/Script.sol"; +import {Creator, Createe} from "../src/Create.sol"; +import {CreatorTwo} from "../src/Create2.sol"; + +contract CreateTestScript is Script { + function setUp() public {} + + function run() public { + vm.startBroadcast(address(0x3f1Eae7D46d88F08fc2F8ed27FCb2AB183EB2d0E)); + Creator creator = new Creator(); + payable(address(creator)).transfer(1 ether); + CreatorTwo creator2 = new CreatorTwo(); + payable(address(creator2)).transfer(1 ether); + address createe = creator.createNoTransferMemUnchanged(); + console.log("createe", createe); + address createe2 = creator2.createTwoNoTransferMemUnchanged(bytes32(uint256(0x1337))); + console.log("createe2", createe2); + address createe3 = creator.createPayableMemUnchanged(); + console.log("createe3", createe3); + address createe4 = creator2.createTwoPayableMemUnchanged(bytes32(uint256(0x1339))); + console.log("createe4", createe4); + creator.createNoTransferMemExpansion(); + creator2.createTwoNoTransferMemExpansion(bytes32(uint256(0x1339))); + creator.createPayableMemExpansion(); + creator2.createTwoPayableMemExpansion(bytes32(uint256(0x1339))); + vm.stopBroadcast(); + } +} diff --git a/gas-dimensions/scripts/DelegateCall.s.sol b/gas-dimensions/scripts/DelegateCall.s.sol new file mode 100644 index 00000000..d0d9036b --- /dev/null +++ b/gas-dimensions/scripts/DelegateCall.s.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Script, VmSafe, console} from "forge-std/Script.sol"; +import {DelegateCaller, DelegateCallee} from "../src//DelegateCall.sol"; + +contract DelegateCallTestScript is Script { + function setUp() public {} + + function run() public { + vm.startBroadcast(address(0x3f1Eae7D46d88F08fc2F8ed27FCb2AB183EB2d0E)); + DelegateCallee callee = new DelegateCallee(); + DelegateCaller caller = new DelegateCaller(); + // this should be warm, to a contract with code + caller.testDelegateCallNonEmptyWarm(address(callee)); + // this should be cold, to a contract with code + caller.testDelegateCallNonEmptyCold(address(callee)); + // this should be cold, to a contract with no code + caller.testDelegateCallEmptyCold(address(0xbeef)); + // this should be warm to a contract with no code + caller.testDelegateCallEmptyWarm(address(0xbeef)); + + // trigger memory expansion + caller.testDelegateCallEmptyWarmMemExpansion(address(0xbeef)); + caller.testDelegateCallNonEmptyWarmMemExpansion(address(callee)); + caller.testDelegateCallEmptyColdMemExpansion(address(0xbeef)); + caller.testDelegateCallNonEmptyColdMemExpansion(address(callee)); + + vm.stopBroadcast(); + } +} diff --git a/gas-dimensions/scripts/ExtCodeCopy.s.sol b/gas-dimensions/scripts/ExtCodeCopy.s.sol new file mode 100644 index 00000000..35aec5d3 --- /dev/null +++ b/gas-dimensions/scripts/ExtCodeCopy.s.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Script, VmSafe} from "forge-std/Script.sol"; +import {Counter} from "../src/Counter.sol"; +import {ExtCodeCopy} from "../src/ExtCodeCopy.sol"; + +contract ExtCodeCopyScript is Script { + function setUp() public {} + + function run() public { + vm.startBroadcast(address(0x3f1Eae7D46d88F08fc2F8ed27FCb2AB183EB2d0E)); + ExtCodeCopy extCodeCopy = new ExtCodeCopy(); + extCodeCopy.extCodeCopyWarmNoMemExpansion(); + extCodeCopy.extCodeCopyColdNoMemExpansion(); + extCodeCopy.extCodeCopyWarmMemExpansion(); + extCodeCopy.extCodeCopyColdMemExpansion(); + vm.stopBroadcast(); + } +} diff --git a/gas-dimensions/scripts/ExtCodeSize.s.sol b/gas-dimensions/scripts/ExtCodeSize.s.sol new file mode 100644 index 00000000..b7ce1ace --- /dev/null +++ b/gas-dimensions/scripts/ExtCodeSize.s.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Script, VmSafe} from "forge-std/Script.sol"; +import {ExtCodeSize} from "../src/ExtCodeSize.sol"; + +contract ExtCodeSizeScript is Script { + function setUp() public {} + + function run() public { + vm.startBroadcast(address(0x3f1Eae7D46d88F08fc2F8ed27FCb2AB183EB2d0E)); + ExtCodeSize extCodeSize = new ExtCodeSize(); + extCodeSize.getExtCodeSizeCold(); + extCodeSize.getExtCodeSizeWarm(); + vm.stopBroadcast(); + } +} \ No newline at end of file diff --git a/gas-dimensions/scripts/GetSlotKey.s.sol b/gas-dimensions/scripts/GetSlotKey.s.sol new file mode 100644 index 00000000..b3e94596 --- /dev/null +++ b/gas-dimensions/scripts/GetSlotKey.s.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Script, VmSafe, console} from "forge-std/Script.sol"; +import {CounterArray} from "../src/CounterArray.sol"; + +contract GetSlotKeyScript is Script { + function setUp() public {} + + function run() public { + vm.startBroadcast(address(0x3f1Eae7D46d88F08fc2F8ed27FCb2AB183EB2d0E)); + CounterArray counterArray = CounterArray(0xA6E41fFD769491a42A6e5Ce453259b93983a22EF); + bytes32 slotKey = counterArray.getSlotKey(3); + console.logBytes32(slotKey); + vm.stopBroadcast(); + } +} diff --git a/gas-dimensions/scripts/Increment.s.sol b/gas-dimensions/scripts/Increment.s.sol new file mode 100644 index 00000000..bb89ebad --- /dev/null +++ b/gas-dimensions/scripts/Increment.s.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Script, VmSafe, console} from "forge-std/Script.sol"; +import {Counter} from "../src/Counter.sol"; + +contract IncrementScript is Script { + function setUp() public {} + + function run() public { + vm.startBroadcast(address(0x3f1Eae7D46d88F08fc2F8ed27FCb2AB183EB2d0E)); + Counter counter = new Counter(); + counter.increment(); + console.log("counter", counter.number()); + vm.stopBroadcast(); + } +} diff --git a/gas-dimensions/scripts/LogEmitter.s.sol b/gas-dimensions/scripts/LogEmitter.s.sol new file mode 100644 index 00000000..63589ac4 --- /dev/null +++ b/gas-dimensions/scripts/LogEmitter.s.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Script, VmSafe, console} from "forge-std/Script.sol"; +import {LogEmitter} from "../src/LogEmitter.sol"; + +contract IncrementScript is Script { + function setUp() public {} + + function run() public { + vm.startBroadcast(address(0x3f1Eae7D46d88F08fc2F8ed27FCb2AB183EB2d0E)); + LogEmitter logEmitter = new LogEmitter(); + logEmitter.emitZeroTopicEmptyData(); + logEmitter.emitZeroTopicNonEmptyData(); + logEmitter.emitOneTopicEmptyData(); + logEmitter.emitOneTopicNonEmptyData(); + logEmitter.emitTwoTopics(); + logEmitter.emitTwoTopicsExtraData(); + logEmitter.emitThreeTopics(); + logEmitter.emitThreeTopicsExtraData(); + logEmitter.emitFourTopics(); + logEmitter.emitFourTopicsExtraData(); + logEmitter.emitZeroTopicNonEmptyDataAndMemExpansion(); + logEmitter.emitOneTopicNonEmptyDataAndMemExpansion(); + logEmitter.emitTwoTopicsExtraDataAndMemExpansion(); + logEmitter.emitThreeTopicsExtraDataAndMemExpansion(); + logEmitter.emitFourTopicsExtraDataAndMemExpansion(); + vm.stopBroadcast(); + } +} diff --git a/gas-dimensions/scripts/NoSpecials.s.sol b/gas-dimensions/scripts/NoSpecials.s.sol new file mode 100644 index 00000000..b52b41f8 --- /dev/null +++ b/gas-dimensions/scripts/NoSpecials.s.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Script, VmSafe, console} from "forge-std/Script.sol"; +import {Counter} from "../src/Counter.sol"; + +contract NoSpecialsScript is Script { + function setUp() public {} + + function run() public { + vm.startBroadcast(address(0x3f1Eae7D46d88F08fc2F8ed27FCb2AB183EB2d0E)); + Counter counter = new Counter(); + counter.noSpecials(); + vm.stopBroadcast(); + } +} diff --git a/gas-dimensions/scripts/OutOfGas.s.sol b/gas-dimensions/scripts/OutOfGas.s.sol new file mode 100644 index 00000000..b618f051 --- /dev/null +++ b/gas-dimensions/scripts/OutOfGas.s.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Script, VmSafe, console} from "forge-std/Script.sol"; +import {OutOfGas} from "../src/OutOfGas.sol"; + +contract OutOfGasScript is Script { + function setUp() public {} + + function run() public { + vm.startBroadcast(address(0x3f1Eae7D46d88F08fc2F8ed27FCb2AB183EB2d0E)); + OutOfGas outOfGas = new OutOfGas(); + outOfGas.callOutOfGas(); + vm.stopBroadcast(); + } +} diff --git a/gas-dimensions/scripts/RefundFromCalldata.s.sol b/gas-dimensions/scripts/RefundFromCalldata.s.sol new file mode 100644 index 00000000..e6853689 --- /dev/null +++ b/gas-dimensions/scripts/RefundFromCalldata.s.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Script, VmSafe, console} from "forge-std/Script.sol"; +import {CounterArray} from "../src//CounterArray.sol"; + +contract RefundFromCalldataScript is Script { + function setUp() public {} + + function run() public { + vm.startBroadcast(address(0x3f1Eae7D46d88F08fc2F8ed27FCb2AB183EB2d0E)); + + bytes32 slotKey = 0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e566; + CounterArray counterArray = CounterArray(0xA6E41fFD769491a42A6e5Ce453259b93983a22EF); + counterArray.refundFromCalldata(slotKey); + vm.stopBroadcast(); + } +} diff --git a/gas-dimensions/scripts/Refunder.s.sol b/gas-dimensions/scripts/Refunder.s.sol new file mode 100644 index 00000000..805fb25f --- /dev/null +++ b/gas-dimensions/scripts/Refunder.s.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Script, VmSafe, console} from "forge-std/Script.sol"; +import {CounterArray} from "../src/CounterArray.sol"; + +contract RefunderScript is Script { + CounterArray public counterArray; + + function setUp() public { + counterArray = new CounterArray(); + uint256[] memory counters = new uint256[](20); + for (uint256 i = 0; i < 20; i++) { + counters[i] = i + 1; + } + counterArray.setCounters(counters); + } + + function run() public { + vm.startBroadcast(address(0x3f1Eae7D46d88F08fc2F8ed27FCb2AB183EB2d0E)); + counterArray.refunder(); + vm.stopBroadcast(); + } +} diff --git a/gas-dimensions/scripts/Refunder1.s.sol b/gas-dimensions/scripts/Refunder1.s.sol new file mode 100644 index 00000000..e4a3558c --- /dev/null +++ b/gas-dimensions/scripts/Refunder1.s.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Script, VmSafe, console} from "forge-std/Script.sol"; +import {CounterArray} from "../src/CounterArray.sol"; + +contract RefunderScript is Script { + function setUp() public {} + + function run() public { + vm.startBroadcast(address(0x3f1Eae7D46d88F08fc2F8ed27FCb2AB183EB2d0E)); + CounterArray counterArray = CounterArray(0x525c2aBA45F66987217323E8a05EA400C65D06DC); + counterArray.refund1(); + vm.stopBroadcast(); + } +} diff --git a/gas-dimensions/scripts/Refunder20.s.sol b/gas-dimensions/scripts/Refunder20.s.sol new file mode 100644 index 00000000..d6aae606 --- /dev/null +++ b/gas-dimensions/scripts/Refunder20.s.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Script, VmSafe, console} from "forge-std/Script.sol"; +import {CounterArray} from "../src/CounterArray.sol"; + +contract RefunderScript is Script { + function setUp() public {} + + function run() public { + vm.startBroadcast(address(0x3f1Eae7D46d88F08fc2F8ed27FCb2AB183EB2d0E)); + CounterArray counterArray = new CounterArray(); + uint256[] memory counters = new uint256[](20); + for (uint256 i = 0; i < 20; i++) { + counters[i] = i + 1; + } + counterArray.setCounters(counters); + //counterArray.refunder(0, 19); + vm.stopBroadcast(); + } +} diff --git a/gas-dimensions/scripts/Refunder21.s.sol b/gas-dimensions/scripts/Refunder21.s.sol new file mode 100644 index 00000000..d8ef4ba9 --- /dev/null +++ b/gas-dimensions/scripts/Refunder21.s.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Script, VmSafe, console} from "forge-std/Script.sol"; +import {CounterArray} from "../src//CounterArray.sol"; + +contract RefunderScript is Script { + function setUp() public {} + + function run() public { + vm.startBroadcast(address(0x3f1Eae7D46d88F08fc2F8ed27FCb2AB183EB2d0E)); + CounterArray counterArray = CounterArray(0xA6E41fFD769491a42A6e5Ce453259b93983a22EF); + counterArray.refunder(); + vm.stopBroadcast(); + } +} diff --git a/gas-dimensions/scripts/RefunderPrint.s.sol b/gas-dimensions/scripts/RefunderPrint.s.sol new file mode 100644 index 00000000..bfb01934 --- /dev/null +++ b/gas-dimensions/scripts/RefunderPrint.s.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Script, VmSafe, console} from "forge-std/Script.sol"; +import {CounterArray} from "../src//CounterArray.sol"; + +contract RefunderScript is Script { + function setUp() public {} + + function run() public { + vm.startBroadcast(address(0x3f1Eae7D46d88F08fc2F8ed27FCb2AB183EB2d0E)); + CounterArray counterArray = CounterArray(0xA6E41fFD769491a42A6e5Ce453259b93983a22EF); + console.log("CounterArray address:", address(counterArray)); + console.log("CounterArray counters[3]:", counterArray.counters(3)); + //counterArray.refunder(0, 19); + vm.stopBroadcast(); + } +} diff --git a/gas-dimensions/scripts/SelfDestructor.s.sol b/gas-dimensions/scripts/SelfDestructor.s.sol new file mode 100644 index 00000000..3ee8d4c5 --- /dev/null +++ b/gas-dimensions/scripts/SelfDestructor.s.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Script, VmSafe} from "forge-std/Script.sol"; +import {SelfDestructor} from "../src//SelfDestructor.sol"; +import {PayableCounter} from "../src//PayableCounter.sol"; + +contract SelfDestructorScript is Script { + function setUp() public {} + + function run() public { + vm.startBroadcast(address(0x3f1Eae7D46d88F08fc2F8ed27FCb2AB183EB2d0E)); + SelfDestructor selfDestructor = new SelfDestructor(); + PayableCounter payableCounter = new PayableCounter(); + + payable(address(selfDestructor)).transfer(0.1 ether); + payable(address(payableCounter)).transfer(0.1 ether); + // warm, code and value at target + selfDestructor.warmSelfDestructor(address(payableCounter)); + + // warm, but there's no money to send + SelfDestructor selfDestructor4 = new SelfDestructor(); + selfDestructor4.warmSelfDestructor(address(payableCounter)); + + // cold, no code or value at target + SelfDestructor selfDestructor2 = new SelfDestructor(); + payable(address(selfDestructor2)).transfer(0.1 ether); + selfDestructor2.selfDestruct(address(0xcafebabe)); + + // cold, code and value at target + SelfDestructor selfDestructor3 = new SelfDestructor(); + payable(address(selfDestructor3)).transfer(0.1 ether); + PayableCounter payableCounter2 = new PayableCounter(); + payable(address(payableCounter2)).transfer(0.1 ether); + selfDestructor3.selfDestruct(address(payableCounter2)); + + // warm but the target address is empty + SelfDestructor selfDestructor5 = new SelfDestructor(); + payable(address(selfDestructor5)).transfer(0.1 ether); + selfDestructor5.warmEmptySelfDestructor(address(0xdeadbeef)); + + vm.stopBroadcast(); + } +} diff --git a/gas-dimensions/scripts/Sload.s.sol b/gas-dimensions/scripts/Sload.s.sol new file mode 100644 index 00000000..909a9ffd --- /dev/null +++ b/gas-dimensions/scripts/Sload.s.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Script, VmSafe, console} from "forge-std/Script.sol"; +import {Sload} from "../src//Sload.sol"; + +contract SloadScript is Script { + function run() public { + vm.startBroadcast(address(0x3f1Eae7D46d88F08fc2F8ed27FCb2AB183EB2d0E)); + Sload sload = new Sload(); + sload.warmSload(); + sload.coldSload(); + vm.stopBroadcast(); + } +} diff --git a/gas-dimensions/scripts/Sstore.s.sol b/gas-dimensions/scripts/Sstore.s.sol new file mode 100644 index 00000000..e99dfe54 --- /dev/null +++ b/gas-dimensions/scripts/Sstore.s.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Script, VmSafe, console} from "forge-std/Script.sol"; +import {Sstore} from "../src//Sstore.sol"; + +contract SstoreScript is Script { + function run() public { + vm.startBroadcast(address(0x3f1Eae7D46d88F08fc2F8ed27FCb2AB183EB2d0E)); + Sstore sstore = new Sstore(); + sstore.sstoreColdZeroToZero(); + sstore.sstoreColdZeroToNonZero(); + sstore.sstoreColdNonZeroValueToZero(); + sstore.sstoreColdNonZeroToSameNonZeroValue(); + sstore.sstoreColdNonZeroToDifferentNonZeroValue(); + sstore.sstoreWarmZeroToZero(); + sstore.sstoreWarmZeroToNonZeroValue(); + sstore.sstoreWarmNonZeroValueToZero(); + sstore.sstoreWarmNonZeroToSameNonZeroValue(); + sstore.sstoreWarmNonZeroToDifferentNonZeroValue(); + sstore.sstoreMultipleWarmNonZeroToNonZeroToNonZero(); + sstore.sstoreMultipleWarmNonZeroToNonZeroToSameNonZero(); + sstore.sstoreMultipleWarmNonZeroToZeroToNonZero(); + sstore.sstoreMultipleWarmNonZeroToZeroToSameNonZero(); + sstore.sstoreMultipleWarmZeroToNonZeroToNonZero(); + sstore.sstoreMultipleWarmZeroToNonZeroBackToZero(); + vm.stopBroadcast(); + } +} diff --git a/gas-dimensions/scripts/StaticCall.s.sol b/gas-dimensions/scripts/StaticCall.s.sol new file mode 100644 index 00000000..c0db3e01 --- /dev/null +++ b/gas-dimensions/scripts/StaticCall.s.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Script, VmSafe, console} from "forge-std/Script.sol"; +import {StaticCallee, StaticCaller} from "../src//StaticCall.sol"; + +contract StaticCallTestScript is Script { + function setUp() public {} + + function run() public { + vm.startBroadcast(address(0x3f1Eae7D46d88F08fc2F8ed27FCb2AB183EB2d0E)); + StaticCallee callee = new StaticCallee(); + StaticCaller caller = new StaticCaller(); + // this should be warm, to a contract with code + caller.testStaticCallNonEmptyWarm(address(callee)); + // this should be cold, to a contract with code + caller.testStaticCallNonEmptyCold(address(callee)); + // this should be cold, to a contract with no code + caller.testStaticCallEmptyCold(address(0xbeef)); + // this should be warm to a contract with no code + caller.testStaticCallEmptyWarm(address(0xbeef)); + // warm, code, mem expansion + caller.testStaticCallNonEmptyWarmMemExpansion(address(callee)); + // warm, no code, mem expansion + caller.testStaticCallEmptyWarmMemExpansion(address(0xbeef)); + // cold, code, no mem expansion + caller.testStaticCallNonEmptyColdMemExpansion(address(callee)); + // cold, no code, no mem expansion + caller.testStaticCallEmptyColdMemExpansion(address(0xbeef)); + + vm.stopBroadcast(); + } +} diff --git a/gas-dimensions/src/Balance.sol b/gas-dimensions/src/Balance.sol new file mode 100644 index 00000000..ed9a70d5 --- /dev/null +++ b/gas-dimensions/src/Balance.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +contract Balance { + uint256 public number; + + function callBalanceCold() public { + uint256 thisBalance = address(this).balance; + uint256 beefbalance = address(0xdeadbeef).balance; + number = thisBalance + beefbalance; + } + + function callBalanceWarm() public { + address target = address(0xdeadbeef); + (bool success,) = target.call{value: 0}(""); + if (success) { + number = 2; + } + uint256 beefbalance2 = target.balance; + number = number + beefbalance2; + } +} diff --git a/gas-dimensions/src/Call.sol b/gas-dimensions/src/Call.sol new file mode 100644 index 00000000..d26d5985 --- /dev/null +++ b/gas-dimensions/src/Call.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +contract Caller { + uint256 public n; + + function _doCallMemUnchanged(address target, uint256 transferValue) internal { + n = 1; + (bool success,) = target.call{value: transferValue}(abi.encodeWithSelector(Callee.setNumber.selector, 0x1337)); + if (success) n = 2; + } + + function _doCallWithMemExpansion(address target, uint256 transferValue) internal { + n = 1; + bytes memory args = abi.encodeWithSelector(Callee.setNumber.selector, 0x1337); + uint256 argsSize = args.length; + bool success; + assembly { + let returnPtr := add(msize(), 0x20) + success := call(gas(), target, transferValue, add(args, 0x20), argsSize, returnPtr, 32) + } + if (success) n = 2; + } + + function warmNoTransferMemUnchanged(address target) public { + uint256 currentValue = target.balance; + _doCallMemUnchanged(target, 0); + if (currentValue == target.balance) n = 3; + } + + function warmPayableMemUnchanged(address target) public { + n = 2; + uint256 currentValue = target.balance; + _doCallMemUnchanged(target, 0xABCD); + if (currentValue == target.balance) n = 3; + } + + function warmNoTransferMemExpansion(address target) public { + uint256 currentValue = target.balance; + _doCallWithMemExpansion(target, 0); + if (currentValue == target.balance) n = 3; + } + + function warmPayableMemExpansion(address target) public { + uint256 currentValue = target.balance; + _doCallWithMemExpansion(target, 0xABCD); + if (currentValue == target.balance) n = 3; + } + + function coldNoTransferMemUnchanged(address target) public { + _doCallMemUnchanged(target, 0); + } + + function coldPayableMemUnchanged(address target) public { + _doCallMemUnchanged(target, 0xABCD); + } + + function coldNoTransferMemExpansion(address target) public { + _doCallWithMemExpansion(target, 0); + } + + function coldPayableMemExpansion(address target) public { + _doCallWithMemExpansion(target, 0xABCD); + } + + receive() external payable {} +} + +contract Callee { + uint256 public number; + + function setNumber(uint256 _number) public payable { + number = _number; + } + + receive() external payable {} +} diff --git a/gas-dimensions/src/Callcode.sol b/gas-dimensions/src/Callcode.sol new file mode 100644 index 00000000..c82ac7c9 --- /dev/null +++ b/gas-dimensions/src/Callcode.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +contract CallCoder { + uint256 public n; + + function _doCallCodeMemUnchanged(address target, uint256 transferValue) internal { + n = 1; + bytes memory args = abi.encodeWithSelector(CallCodee.setNumber.selector, 0x1337); + uint256 argsSize = args.length; + uint256 returnValue; + bool success; + assembly { + success := callcode(gas(), target, transferValue, add(args, 0x20), argsSize, returnValue, 32) + } + if (success) n = 2; + } + + function _doCallCodeWithMemExpansion(address target, uint256 transferValue) internal { + n = 1; + bytes memory args = abi.encodeWithSelector(CallCodee.setNumber.selector, 0x1337); + uint256 argsSize = args.length; + bool success; + assembly { + let returnPtr := add(msize(), 0x20) + success := callcode(gas(), target, transferValue, add(args, 0x20), argsSize, returnPtr, 32) + } + if (success) n = 2; + } + + function warmNoTransferMemUnchanged(address target) public { + uint256 currentValue = target.balance; + _doCallCodeMemUnchanged(target, 0); + if (currentValue == target.balance) n = 3; + } + + function warmPayableMemUnchanged(address target) public { + n = 2; + uint256 currentValue = target.balance; + _doCallCodeMemUnchanged(target, 0xABCD); + if (currentValue == target.balance) n = 3; + } + + function warmNoTransferMemExpansion(address target) public { + uint256 currentValue = target.balance; + _doCallCodeWithMemExpansion(target, 0); + if (currentValue == target.balance) n = 3; + } + + function warmPayableMemExpansion(address target) public { + uint256 currentValue = target.balance; + _doCallCodeWithMemExpansion(target, 0xABCD); + if (currentValue == target.balance) n = 3; + } + + function coldNoTransferMemUnchanged(address target) public { + _doCallCodeMemUnchanged(target, 0); + } + + function coldPayableMemUnchanged(address target) public { + _doCallCodeMemUnchanged(target, 0xABCD); + } + + function coldNoTransferMemExpansion(address target) public { + _doCallCodeWithMemExpansion(target, 0); + } + + function coldPayableMemExpansion(address target) public { + _doCallCodeWithMemExpansion(target, 0xABCD); + } + + receive() external payable {} +} + +contract CallCodee { + uint256 public n; + + function setNumber(uint256 _n) public payable { + n = _n; + } + + receive() external payable {} +} diff --git a/gas-dimensions/src/Counter.sol b/gas-dimensions/src/Counter.sol new file mode 100644 index 00000000..b4e7a242 --- /dev/null +++ b/gas-dimensions/src/Counter.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +contract Counter { + uint256 public number; + + function setNumber(uint256 newNumber) public { + number = newNumber; + } + + function increment() public { + uint256 newNumber = number; // make extra sure to cause a cold SLOAD + newNumber = newNumber + 1; + uint256 oldNumber = number; // make sure to cause a warm SLOAD + number = newNumber + oldNumber; + } + + function noSpecials() public { + assembly { + let x := add(0x1337, 0x6969) + return(x, 0x20) + } + } +} diff --git a/gas-dimensions/src/CounterArray.sol b/gas-dimensions/src/CounterArray.sol new file mode 100644 index 00000000..0f47cbbc --- /dev/null +++ b/gas-dimensions/src/CounterArray.sol @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +contract CounterArray { + uint256[] public counters; + + // create the array + constructor() { + counters = new uint256[](20); + } + + // alllow caller to set data in the array + function setCounters(uint256[] memory newCounters) public { + require(newCounters.length == 20, "Array must be 20 elements long"); + for (uint256 i = 0; i < 20; i++) { + counters[i] = newCounters[i]; + } + } + + function refund1() public { + assembly { + let slot := counters.slot + let slotKey := keccak256(slot, 0x20) + let data := sload(slotKey) + sstore(slotKey, 0) + } + } + + function getSlotKey(uint256 index) public pure returns (bytes32) { + // Declare variable in Solidity scope + bytes32 elementSlot; + assembly { + let slot := counters.slot + let slotKey := keccak256(slot, 0x20) + // Assign to the Solidity variable using := + elementSlot := add(slotKey, index) + } + return elementSlot; + } + + function refundFromCalldata(bytes32 slotKey) public { + assembly { + sstore(slotKey, 0) + } + } + + // manually SSTORE to zero out the storage + // unrolled loop assuming 20 elements + function refunder() public { + assembly { + // Get the slot of the counters array + let slot := counters.slot + // + let slotKey := keccak256(slot, 0x20) + // Zero out the storage slot + sstore(slotKey, 0) + let elementSlot := add(slotKey, 1) + // Zero out the storage slot + sstore(elementSlot, 0) + elementSlot := add(slotKey, 2) + // Zero out the storage slot + sstore(elementSlot, 0) + elementSlot := add(slotKey, 3) + // Zero out the storage slot + sstore(elementSlot, 0) + elementSlot := add(slotKey, 4) + // Zero out the storage slot + sstore(elementSlot, 0) + elementSlot := add(slotKey, 5) + // Zero out the storage slot + sstore(elementSlot, 0) + elementSlot := add(slotKey, 6) + // Zero out the storage slot + sstore(elementSlot, 0) + elementSlot := add(slotKey, 7) + // Zero out the storage slot + sstore(elementSlot, 0) + elementSlot := add(slotKey, 8) + // Zero out the storage slot + sstore(elementSlot, 0) + elementSlot := add(slotKey, 9) + // Zero out the storage slot + sstore(elementSlot, 0) + elementSlot := add(slotKey, 10) + // Zero out the storage slot + sstore(elementSlot, 0) + elementSlot := add(slotKey, 11) + // Zero out the storage slot + sstore(elementSlot, 0) + elementSlot := add(slotKey, 12) + // Zero out the storage slot + sstore(elementSlot, 0) + elementSlot := add(slotKey, 13) + // Zero out the storage slot + sstore(elementSlot, 0) + elementSlot := add(slotKey, 14) + // Zero out the storage slot + sstore(elementSlot, 0) + elementSlot := add(slotKey, 15) + // Zero out the storage slot + sstore(elementSlot, 0) + elementSlot := add(slotKey, 16) + // Zero out the storage slot + sstore(elementSlot, 0) + elementSlot := add(slotKey, 17) + // Zero out the storage slot + sstore(elementSlot, 0) + elementSlot := add(slotKey, 18) + // Zero out the storage slot + sstore(elementSlot, 0) + elementSlot := add(slotKey, 19) + // Zero out the storage slot + sstore(elementSlot, 0) + } + } +} diff --git a/gas-dimensions/src/Create.sol b/gas-dimensions/src/Create.sol new file mode 100644 index 00000000..bcfc287c --- /dev/null +++ b/gas-dimensions/src/Create.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +contract Createe { + uint256 public x; + + constructor(uint256 _x) payable { + x = _x; + } + + receive() external payable {} +} + +contract Creator { + receive() external payable {} + + function _doCreateMemUnchanged(uint256 amount, uint256 arg) internal returns (address) { + Createe createe = new Createe{value: amount}(arg); + return address(createe); + + } + + // arg doesn't matter here because we don't actually call the constructor LOL + function _doCreateMemExpansion(uint256 amount) internal { + bytes memory code = type(Createe).creationCode; + assembly { + let m := msize() + let offsetDiff := add(sub(m, code), 0x20) + let unused :=create(amount, add(code, 0x20), offsetDiff) + pop(unused) + } + } + + function createNoTransferMemUnchanged() public returns (address) { + return _doCreateMemUnchanged(0x0, 0x42); + } + + function createNoTransferMemExpansion() public { + _doCreateMemExpansion(0x0); + } + + function createPayableMemUnchanged() public payable returns (address) { + return _doCreateMemUnchanged(0x69420, 0x42); + } + + function createPayableMemExpansion() public payable { + _doCreateMemExpansion(0x69420); + } +} diff --git a/gas-dimensions/src/Create2.sol b/gas-dimensions/src/Create2.sol new file mode 100644 index 00000000..121dcbd8 --- /dev/null +++ b/gas-dimensions/src/Create2.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Createe} from "./Create.sol"; + +contract CreatorTwo { + receive() external payable {} + + function _doCreateTwoMemUnchanged(uint256 amount, uint256 arg, bytes32 salt) internal returns (address) { + Createe createe = new Createe{value: amount, salt: salt}(arg); + return address(createe); + + } + + // arg doesn't matter here because we don't actually call the constructor LOL + function _doCreateTwoMemExpansion(uint256 amount, bytes32 salt) internal { + bytes memory code = type(Createe).creationCode; + assembly { + let m := msize() + let offsetDiff := add(sub(m, code), 0x20) + let unused :=create2(amount, add(code, 0x20), offsetDiff, salt) + pop(unused) + } + } + + function createTwoNoTransferMemUnchanged(bytes32 _salt) public returns (address) { + return _doCreateTwoMemUnchanged(0x0, 0x42, _salt); + } + + function createTwoNoTransferMemExpansion(bytes32 _salt) public { + _doCreateTwoMemExpansion(0x0, _salt); + } + + function createTwoPayableMemUnchanged(bytes32 _salt) public payable returns (address) { + return _doCreateTwoMemUnchanged(0x69420, 0x42, _salt); + } + + function createTwoPayableMemExpansion(bytes32 _salt) public payable { + _doCreateTwoMemExpansion(0x42069, _salt); + } +} + diff --git a/gas-dimensions/src/DelegateCall.sol b/gas-dimensions/src/DelegateCall.sol new file mode 100644 index 00000000..acafe57d --- /dev/null +++ b/gas-dimensions/src/DelegateCall.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +contract DelegateCaller { + uint256 public value; + + function testDelegateCallEmptyWarm(address emptyAddress) public { + // Warm up the address in the access list by accessing its balance + uint256 balance = emptyAddress.balance; + // Perform delegatecall to empty address + (bool success,) = emptyAddress.delegatecall(""); + if (success && balance > 0) { + value = 4; + } + } + + function testDelegateCallNonEmptyWarm(address nonEmptyAddress) public { + // Warm up the address in the access list by accessing its balance + uint256 balance = nonEmptyAddress.balance; + // Perform delegatecall to non-empty address + (bool success,) = nonEmptyAddress.delegatecall(abi.encodeWithSignature("setValue(uint256)", 42)); + if (success && balance > 0) { + value = 4; + } + } + + function testDelegateCallEmptyCold(address emptyAddress) public { + // Perform delegatecall to empty address + (bool success,) = emptyAddress.delegatecall(""); + if (success) { + value = 4; + } + } + + function testDelegateCallNonEmptyCold(address nonEmptyAddress) public { + // Perform delegatecall to non-empty address + (bool success,) = nonEmptyAddress.delegatecall(abi.encodeWithSignature("setValue(uint256)", 42)); + if (success) { + value = 4; + } + } + + function testDelegateCallEmptyWarmMemExpansion(address emptyAddress) public { + // Warm up the address in the access list by accessing its balance + uint256 balance = emptyAddress.balance; + bool success; + assembly { + let ptr := msize() + let gasLeft := gas() + success := delegatecall(gasLeft, emptyAddress, ptr, 0x40, ptr, 0x40) + } + if (success && balance == 0) { + value = 3; + } else { + value = 4; + } + } + + function testDelegateCallNonEmptyWarmMemExpansion(address nonEmptyAddress) public { + // Warm up the address in the access list by accessing its balance + uint256 balance = nonEmptyAddress.balance; + bytes memory args = abi.encodeWithSignature("setValue(uint256)", 43); + uint256 argsSize = args.length; + bool success; + assembly { + let ptr := msize() + let gasLeft := gas() + success := delegatecall(gasLeft, nonEmptyAddress, add(args, 0x20), argsSize, ptr, 0x40) + } + if (success && balance == 0) { + value = 3; + } else { + value = 4; + } + } + + function testDelegateCallEmptyColdMemExpansion(address emptyAddress) public { + bool success; + assembly { + let ptr := msize() + let gasLeft := gas() + success := delegatecall(gasLeft, emptyAddress, ptr, 0x40, ptr, 0x40) + } + if (success) { + value = 3; + } else { + value = 4; + } + } + + function testDelegateCallNonEmptyColdMemExpansion(address nonEmptyAddress) public { + bytes memory args = abi.encodeWithSignature("setValue(uint256)", 42); + uint256 argsSize = args.length; + bool success; + assembly { + let ptr := msize() + let gasLeft := gas() + success := delegatecall(gasLeft, nonEmptyAddress, add(args, 0x20), argsSize, ptr, 0x40) + } + if (success) { + value = 4; + } + } +} + +contract DelegateCallee { + // Storage layout must match the calling contract for delegatecall to work as expected + uint256 public value; + + /** + * @dev Set the value in storage + * When called via delegatecall, this will modify the storage of the calling contract + */ + function setValue(uint256 _value) public returns (bool) { + value = _value; + return true; + } +} diff --git a/gas-dimensions/src/ExtCodeCopy.sol b/gas-dimensions/src/ExtCodeCopy.sol new file mode 100644 index 00000000..94da4d6d --- /dev/null +++ b/gas-dimensions/src/ExtCodeCopy.sol @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Counter} from "./Counter.sol"; + +contract ExtCodeCopy { + bytes32 knownHash; + Counter counter; + Counter coldCounter; + + constructor() { + counter = new Counter(); + coldCounter = new Counter(); + } + + function extCodeCopyWarmNoMemExpansion() public returns (bytes32 codeHash) { + uint256 codeSize; + address contractAddress = address(counter); + assembly { + codeSize := extcodesize(contractAddress) // forces warm access state + } + + // Create a much larger buffer to force memory expansion ahead of time + bytes memory localCode = new bytes(codeSize); + + assembly { + // should not trigger memory expansion + extcodecopy( + contractAddress, // address to copy from + add(localCode, 32), // skip first 32 bytes which contain length + 0, // start reading from this position in the code + codeSize // number of bytes to copy + ) + } + + // Hash the local code + codeHash = keccak256(localCode); + knownHash = codeHash; + } + + function extCodeCopyColdNoMemExpansion() public returns (bytes32 codeHash) { + uint256 codeSize; + address contractAddress = address(counter); + assembly { + codeSize := extcodesize(contractAddress) + } + + address coldAddress = address(coldCounter); + + // Create a much larger buffer to force memory expansion ahead of time + bytes memory localCode = new bytes(codeSize); + + assembly { + // should not trigger memory expansion + extcodecopy( + coldAddress, // address to copy from + add(localCode, 32), // skip first 32 bytes which contain length + 0, // start reading from this position in the code + codeSize // number of bytes to copy + ) + } + + // Hash the local code + codeHash = keccak256(localCode); + knownHash = codeHash; + } + + function extCodeCopyWarmMemExpansion() public returns (bytes32 codeHash) { + uint256 codeSize; + address contractAddress = address(counter); + assembly { + codeSize := extcodesize(contractAddress) // forces warm access state + } + bytes memory localCode = new bytes(codeSize); + assembly { + let mSize := msize() + extcodecopy( + contractAddress, // address to copy from + sub(mSize, 1), // place it in the last few bytes of memory to force expansion + 0, // start reading from this position in the code + codeSize // number of bytes to copy + ) + } + + // Hash the local code + codeHash = keccak256(localCode); + knownHash = codeHash; + } + + function extCodeCopyColdMemExpansion() public returns (bytes32 codeHash) { + uint256 codeSize; + address contractAddress = address(counter); + assembly { + codeSize := extcodesize(contractAddress) + } + + address coldAddress = address(coldCounter); + bytes memory localCode = new bytes(codeSize); + assembly { + let mSize := msize() + extcodecopy( + coldAddress, // address to copy from + sub(mSize, 1), // place it in the last few bytes of memory to force expansion + 0, // start reading from this position in the code + codeSize // number of bytes to copy + ) + } + + // Hash the local code + codeHash = keccak256(localCode); + knownHash = codeHash; + } +} diff --git a/gas-dimensions/src/ExtCodeHash.sol b/gas-dimensions/src/ExtCodeHash.sol new file mode 100644 index 00000000..0b748b22 --- /dev/null +++ b/gas-dimensions/src/ExtCodeHash.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +contract ExtCodeHashCallee { + uint256 public number; + + function setNumber(uint256 _number) public { + number = _number; + } +} + +contract ExtCodeHash { + ExtCodeHashCallee callee; + bool q; + + constructor() { + callee = new ExtCodeHashCallee(); + } + + function getExtCodeHashCold() public returns (bytes32) { + bytes32 ret = address(callee).codehash; + q = true; + return ret; + } + + function getExtCodeHashWarm() public returns (bytes32) { + (bool success,) = + address(callee).call{value: 0}(abi.encodeWithSelector(ExtCodeHashCallee.setNumber.selector, 0x1337)); + if (success) { + q = true; + } + return address(callee).codehash; + } +} diff --git a/gas-dimensions/src/ExtCodeSize.sol b/gas-dimensions/src/ExtCodeSize.sol new file mode 100644 index 00000000..847e6691 --- /dev/null +++ b/gas-dimensions/src/ExtCodeSize.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +contract ExtCodeSizeCallee { + uint256 public number; + + function setNumber(uint256 _number) public { + number = _number; + } +} + +contract ExtCodeSize { + ExtCodeSizeCallee callee; + bool q; + + constructor() { + callee = new ExtCodeSizeCallee(); + } + + function getExtCodeSizeCold() public returns (uint256) { + uint256 ret = address(callee).code.length; + q = true; + return ret; + } + + function getExtCodeSizeWarm() public returns (uint256) { + (bool success,) = + address(callee).call{value: 0}(abi.encodeWithSelector(ExtCodeSizeCallee.setNumber.selector, 0x1337)); + if (success) { + q = true; + } + return address(callee).code.length; + } +} diff --git a/gas-dimensions/src/LogEmitter.sol b/gas-dimensions/src/LogEmitter.sol new file mode 100644 index 00000000..7d6e99c2 --- /dev/null +++ b/gas-dimensions/src/LogEmitter.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +contract LogEmitter { + uint256 public number; + + event LogOneTopic(); + event LogOneTopicExtraData(bytes); + event LogTwoTopics(uint256 indexed number); + event LogTwoTopicsExtraData(uint256 indexed number, address); + event LogThreeTopics(uint256 indexed number, address indexed addy); + event LogThreeTopicsExtraData(uint256 indexed number, address indexed addy, bytes32); + event LogFourTopics(uint256 indexed number, address indexed addy, bytes32 indexed keccakHash); + event LogFourTopicsExtraData(uint256 indexed number, address indexed addy, bytes32 indexed keccakHash, bytes32); + + function logZeroTopics(bytes memory data) internal { + assembly { + log0(add(data, 0x20), mload(data)) + } + } + + function emitZeroTopicEmptyData() public { + logZeroTopics(""); + number++; + } + + function emitZeroTopicNonEmptyData() public { + logZeroTopics(abi.encodePacked("abcdefg")); + number++; + } + + function emitZeroTopicNonEmptyDataAndMemExpansion() public { + assembly { + let memPtr := msize() + log0(memPtr, 0x40) + } + number++; + } + + function emitOneTopicEmptyData() public { + emit LogOneTopic(); + number++; + } + + function emitOneTopicNonEmptyData() public { + bytes memory data = abi.encodePacked("hijklmnop"); + bytes32 topic = bytes32(uint256(0x1337)); + assembly { + log1(add(data, 0x20), mload(data), topic) + } + number++; + } + + function emitOneTopicNonEmptyDataAndMemExpansion() public { + bytes32 topic = bytes32(uint256(0x1337)); + assembly { + let memPtr := msize() + log1(memPtr, 0x40, topic) + } + number++; + } + + function emitTwoTopics() public { + emit LogTwoTopics(0xd00d00); + number++; + } + + function emitTwoTopicsExtraData() public { + emit LogTwoTopicsExtraData(0xcaca, address(0xdeadcafe)); + number++; + } + + function emitTwoTopicsExtraDataAndMemExpansion() public { + bytes32 topic = bytes32(uint256(0x1337)); + bytes32 topic2 = bytes32(uint256(0x1338)); + assembly { + let memPtr := msize() + log2(memPtr, 0x40, topic, topic2) + } + number++; + } + + function emitThreeTopics() public { + emit LogThreeTopics(0xb1337b, address(0xbabecafe)); + number++; + } + + function emitThreeTopicsExtraData() public { + emit LogThreeTopicsExtraData(0xb00bb00c, address(0xfeedface), bytes32(abi.encodePacked("HIJKLMNOP"))); + number++; + } + + function emitThreeTopicsExtraDataAndMemExpansion() public { + bytes32 topic = bytes32(uint256(0x1337)); + bytes32 topic2 = bytes32(uint256(0x1338)); + bytes32 topic3 = bytes32(uint256(0x1339)); + assembly { + let memPtr := msize() + log3(memPtr, 0x40, topic, topic2, topic3) + } + number++; + } + + function emitFourTopics() public { + emit LogFourTopics(0xfadedb00b, address(0xbeeffeed), keccak256(abi.encodePacked("QRSTUVWXYZ"))); + number++; + } + + function emitFourTopicsExtraData() public { + emit LogFourTopicsExtraData( + 0xdaff0d177, address(0xfeedcafe), keccak256(abi.encodePacked("ABCD")), bytes32(abi.encodePacked("ABCDEFG")) + ); + number++; + } + + function emitFourTopicsExtraDataAndMemExpansion() public { + bytes32 topic = bytes32(uint256(0x1337)); + bytes32 topic2 = bytes32(uint256(0x1338)); + bytes32 topic3 = bytes32(uint256(0x1339)); + bytes32 topic4 = bytes32(uint256(0x133a)); + assembly { + let memPtr := msize() + log4(memPtr, 0x40, topic, topic2, topic3, topic4) + } + number++; + } +} diff --git a/gas-dimensions/src/OutOfGas.sol b/gas-dimensions/src/OutOfGas.sol new file mode 100644 index 00000000..009b73db --- /dev/null +++ b/gas-dimensions/src/OutOfGas.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract OutOfGas { + uint256 public x; + bool public gasErrorOccurred; + + function outOfGas() public { + while (true) { + // This loop will run forever, consuming all gas + x++; + } + } + + function callOutOfGas() public { + gasErrorOccurred = false; + try this.outOfGas{gas: 100000}() { + // This block will never execute because outOfGas always fails + gasErrorOccurred = false; + } catch { + // This block will execute when outOfGas runs out of gas + gasErrorOccurred = true; + } + } +} diff --git a/gas-dimensions/src/PayableCounter.sol b/gas-dimensions/src/PayableCounter.sol new file mode 100644 index 00000000..d41064fa --- /dev/null +++ b/gas-dimensions/src/PayableCounter.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +contract PayableCounter { + uint256 public number; + + function setNumber(uint256 newNumber) public { + number = newNumber; + } + + fallback() external payable {} + + receive() external payable {} +} diff --git a/gas-dimensions/src/SelfDestructor.sol b/gas-dimensions/src/SelfDestructor.sol new file mode 100644 index 00000000..d60a8079 --- /dev/null +++ b/gas-dimensions/src/SelfDestructor.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Counter} from "./Counter.sol"; + +contract SelfDestructor { + function warmSelfDestructor(address who) public { + Counter counter = Counter(who); + counter.setNumber(1); + selfDestruct(who); + } + + function warmEmptySelfDestructor(address who) public { + (bool success,) = who.call(""); + selfDestruct(who); + } + + function selfDestruct(address who) public { + selfdestruct(payable(who)); + } + + receive() external payable {} + + fallback() external payable {} +} diff --git a/gas-dimensions/src/Sload.sol b/gas-dimensions/src/Sload.sol new file mode 100644 index 00000000..cef737b1 --- /dev/null +++ b/gas-dimensions/src/Sload.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract Sload { + uint256 public a; + + constructor() { + a = 3; + } + + function warmSload() public returns (uint256) { + a = 4; + uint256 b = a; + return b; + } + + function coldSload() public returns (uint256) { + uint256 b = a; + a = 5; + return b; + } +} diff --git a/gas-dimensions/src/Sstore.sol b/gas-dimensions/src/Sstore.sol new file mode 100644 index 00000000..861e606b --- /dev/null +++ b/gas-dimensions/src/Sstore.sol @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract Sstore { + uint256 public zeroFromStart; + uint256 public nonZeroFromStart; + + constructor() { + nonZeroFromStart = 2; + } + + function sstoreColdZeroToZero() public { + zeroFromStart = 0; + } + + function sstoreColdZeroToNonZero() public { + zeroFromStart = 1; + } + + function sstoreColdNonZeroValueToZero() public { + nonZeroFromStart = 0; + } + + function sstoreColdNonZeroToSameNonZeroValue() public { + nonZeroFromStart = 2; + } + + function sstoreColdNonZeroToDifferentNonZeroValue() public { + nonZeroFromStart = 3; + } + + function sstoreWarmZeroToZero() public returns (uint256) { + uint256 x = zeroFromStart; + zeroFromStart = 0; + return x; + } + + function sstoreWarmZeroToNonZeroValue() public returns (uint256) { + uint256 x = zeroFromStart; + zeroFromStart = 1; + return x; + } + + function sstoreWarmNonZeroValueToZero() public returns (uint256) { + uint256 x = nonZeroFromStart; + nonZeroFromStart = 0; + return x; + } + + function sstoreWarmNonZeroToSameNonZeroValue() public returns (uint256) { + uint256 x = nonZeroFromStart; + nonZeroFromStart = 2; + return x; + } + + function sstoreWarmNonZeroToDifferentNonZeroValue() public returns (uint256) { + uint256 x = nonZeroFromStart; + nonZeroFromStart = 3; + return x; + } + + function sstoreMultipleWarmNonZeroToNonZeroToNonZero() public returns (uint256) { + uint256 x = nonZeroFromStart; + nonZeroFromStart = 3; + x = nonZeroFromStart; + nonZeroFromStart = 4; + return x; + } + + function sstoreMultipleWarmNonZeroToNonZeroToSameNonZero() public returns (uint256) { + uint256 x = nonZeroFromStart; + nonZeroFromStart = 3; + x = nonZeroFromStart; + nonZeroFromStart = 2; + return x; + } + + function sstoreMultipleWarmNonZeroToZeroToNonZero() public returns (uint256) { + uint256 x = nonZeroFromStart; + nonZeroFromStart = 0; + x = nonZeroFromStart; + nonZeroFromStart = 4; + return x; + } + + function sstoreMultipleWarmNonZeroToZeroToSameNonZero() public returns (uint256) { + uint256 x = nonZeroFromStart; + nonZeroFromStart = 0; + x = nonZeroFromStart; + nonZeroFromStart = 2; + return x; + } + + function sstoreMultipleWarmZeroToNonZeroToNonZero() public returns (uint256) { + uint256 x = zeroFromStart; + zeroFromStart = 1; + x = zeroFromStart; + zeroFromStart = 2; + return x; + } + + function sstoreMultipleWarmZeroToNonZeroBackToZero() public returns (uint256) { + uint256 x = zeroFromStart; + zeroFromStart = 1; + x = zeroFromStart; + zeroFromStart = 0; + return x; + } +} diff --git a/gas-dimensions/src/StaticCall.sol b/gas-dimensions/src/StaticCall.sol new file mode 100644 index 00000000..6dbe5e20 --- /dev/null +++ b/gas-dimensions/src/StaticCall.sol @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +contract StaticCaller { + uint256 public n; + + function _doStaticCall(address target) internal returns (bool success) { + bytes memory returnData = new bytes(32); + bytes memory args = abi.encodeWithSignature("getNumber()"); + uint256 argsSize = args.length; + assembly { + success := + staticcall( + gas(), // Forward all available gas + target, // Target contract + add(args, 32), // Input data (skip length) + argsSize, // Input size + add(returnData, 32), // Output location (skip length) + 32 // Output size (32 bytes for uint256) + ) + } + if (success) { + n = uint256(bytes32(returnData)); + } else { + n = 0x1337; + } + } + + function testStaticCallEmptyWarm(address emptyAddress) public { + uint256 bal = emptyAddress.balance; + bool success = _doStaticCall(emptyAddress); + if (success && bal == 0) { + n = 0x42; + } else { + n = 0x1337; + } + } + + function testStaticCallNonEmptyWarm(address nonEmptyAddress) public { + uint256 bal = nonEmptyAddress.balance; + bool success = _doStaticCall(nonEmptyAddress); + if (success && bal == 0) { + n = 0x42; + } else { + n = 0x1337; + } + } + + function testStaticCallEmptyCold(address emptyAddress) public { + _doStaticCall(emptyAddress); + } + + function testStaticCallNonEmptyCold(address nonEmptyAddress) public { + _doStaticCall(nonEmptyAddress); + } + + // for valid non empty static calls, force the memory expansion in the + // return data + function _doNonEmptyStaticCallMemExpanded(address target) internal returns (bool success) { + bytes memory args = abi.encodeWithSignature("getNumber()"); + uint256 argsSize = args.length; + assembly { + // do NOT allocate memory for the return data + // instead send a return pointer that is way past the end of memory + let returnPtr := add(msize(), 0x20) + let gasLeft := gas() + // Make the static call + success := staticcall(gasLeft, target, add(args, 0x20), argsSize, returnPtr, 32) + } + if (success) { + n = 1; + } else { + n = 0x1337; + } + } + + // for invalid empty static calls, since the contract does not have code to call + // there will be no return data so we have to force the memory expansion on the args + function _doEmptyStaticCallMemExpanded(address target) internal returns (bool success) { + bytes memory returnData; + assembly { + // do NOT allocate memory for the return data + // instead send a return pointer that is way past the end of memory + let ptr := add(msize(), 0x20) + let gasLeft := gas() + // Make the static call + success := staticcall(gasLeft, target, ptr, 4, ptr, 32) + // Update the free memory pointer + // We need space for success flag (32 bytes) + return data (32 bytes) + mstore(0x40, add(ptr, 0x20)) + if success { returnData := mload(ptr) } + } + if (success) { + n = uint256(bytes32(returnData)); + } else { + n = 0x1337; + } + } + + function testStaticCallEmptyWarmMemExpansion(address emptyAddress) public { + uint256 bal = emptyAddress.balance; + bool success = _doEmptyStaticCallMemExpanded(emptyAddress); + if (success && bal == 0) { + n = 0x42; + } else { + n = 0x1337; + } + } + + function testStaticCallNonEmptyWarmMemExpansion(address nonEmptyAddress) public { + uint256 bal = nonEmptyAddress.balance; + bool success = _doNonEmptyStaticCallMemExpanded(nonEmptyAddress); + if (success && bal == 0) { + n = 0x42; + } else { + n = 0x1337; + } + } + + function testStaticCallEmptyColdMemExpansion(address emptyAddress) public { + _doEmptyStaticCallMemExpanded(emptyAddress); + } + + function testStaticCallNonEmptyColdMemExpansion(address nonEmptyAddress) public { + _doNonEmptyStaticCallMemExpanded(nonEmptyAddress); + } +} + +contract StaticCallee { + uint256 private number; + + constructor() { + number = 0x414243; + } + + function getNumber() public view returns (uint256) { + return number; + } +} diff --git a/package.json b/package.json index 781e82be..9a3183b9 100644 --- a/package.json +++ b/package.json @@ -24,9 +24,10 @@ "coverage": "forge coverage --report lcov --ir-minimum && lcov --remove lcov.info 'node_modules/*' 'test/*' 'script/*' 'src/test-helpers/*' 'challenge/*' --ignore-errors unused -o lcov.info && genhtml lcov.info --branch-coverage --output-dir coverage", "build:all": "yarn build && yarn build:forge", "build": "hardhat compile", + "build:forge:gas-dimensions": "FOUNDRY_PROFILE=gas-dimensions forge build", "build:forge:sol": "forge build --skip *.yul", "build:forge:yul": "FOUNDRY_PROFILE=yul forge build --skip *.sol", - "build:forge": "yarn build:forge:sol && yarn build:forge:yul", + "build:forge": "yarn build:forge:sol && yarn build:forge:yul && yarn build:forge:gasdimensions", "contract:size": "hardhat size-contracts", "lint:test": "eslint ./test", "solhint": "solhint -f table src/**/*.sol",