├── .github └── workflows │ └── test.yml ├── .gitignore ├── .gitmodules ├── README.md ├── foundry.toml ├── script └── Counter.s.sol ├── src ├── ConstantGasExternalLibrary.sol └── NonConstantGasExternalLibrary.sol └── test └── ConstantGasExternalLibrary.t.sol /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: workflow_dispatch 4 | 5 | env: 6 | FOUNDRY_PROFILE: ci 7 | 8 | jobs: 9 | check: 10 | strategy: 11 | fail-fast: true 12 | 13 | name: Foundry project 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | submodules: recursive 19 | 20 | - name: Install Foundry 21 | uses: foundry-rs/foundry-toolchain@v1 22 | with: 23 | version: nightly 24 | 25 | - name: Run Forge build 26 | run: | 27 | forge --version 28 | forge build --sizes 29 | id: build 30 | 31 | - name: Run Forge tests 32 | run: | 33 | forge test -vvv 34 | id: test 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | 5 | # Ignores development broadcast logs 6 | !/broadcast 7 | /broadcast/*/31337/ 8 | /broadcast/**/dry-run/ 9 | 10 | # Docs 11 | docs/ 12 | 13 | # Dotenv file 14 | .env 15 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Constant Gas Function Dispatcher for Better Optimized Solidity External Libraries 2 | 3 | **Unaudited.** 4 | 5 | This eliminates the need to use of 4 byte function selectors in our external libraries and uses as low as 1 byte of calldata to identify a jumpdest in the library's runtime code to jump to 6 | 7 | It saves a minimum of 48 gas (16 \* 3) + (22 \* n) gas where n is the index of the function if it were to be on a chronological jumptable. 8 | 9 | Note: Due to the new code generator via_ir uses, compiling contracts that use this pattern with via_ir set to true will not have as much efficiency as ones with via_ir set to false. 10 | This is because the new codegen uses internal IDs starting from 1 as what internal function pointers hold, then at runtime it mimics a switch statement to find the right internal function to jump to. This is essentially the same as a linear jumpdest which we are trying to avoid in the first place. So it is recommended to turn off via_ir when compiling contracts that use this pattern 11 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | libs = ["lib"] 5 | bytecode_hash = "none" 6 | cbor_metadata = false 7 | 8 | # See more config options https://github.com/foundry-rs/foundry/tree/master/config 9 | -------------------------------------------------------------------------------- /script/Counter.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import {Script, console2} from "forge-std/Script.sol"; 5 | 6 | contract CounterScript is Script { 7 | function setUp() public {} 8 | 9 | function run() public { 10 | vm.broadcast(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/ConstantGasExternalLibrary.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | contract ConstantGasExternalLibrary { 5 | uint256 totalSupply; 6 | mapping(address addr => uint256 balance) _balanceOf; 7 | 8 | constructor(bool shouldDeploy) { 9 | /// if true, it deploys successfully 10 | /// if false, it reverts with the concatenation of the creation time and runtime jumpdests of each of the 3 functions 11 | /// each jumpdest is 4 bytes so each concatenated jumpdest is 8 bytes. (each pair of creation and runtime jumpdests are stored in a word) 12 | if (!shouldDeploy) { 13 | function () internal _add = add; 14 | function () internal _sub = sub; 15 | function () internal _mul = mul; 16 | 17 | assembly { 18 | mstore(0x00, _add) 19 | mstore(0x20, _sub) 20 | mstore(0x40, _mul) 21 | revert(0x00, 0x60) 22 | } 23 | } 24 | } 25 | 26 | fallback() external payable { 27 | // first byte of calldata represents jumpdest for the operation 28 | // other parameters are abi encodepacked. 29 | function () internal action; 30 | assembly { 31 | let jumpDest := shr(248, calldataload(0x00)) 32 | action := jumpDest 33 | } 34 | 35 | action(); 36 | 37 | // execution will not get here. 38 | // i add this because without a dependence of an internal function by an external or public function, solidity will exclude it from the runtime code 39 | add(); 40 | sub(); 41 | mul(); 42 | } 43 | 44 | function add() internal { 45 | // returns bool success 46 | 47 | // declare paramters 48 | uint256 a; 49 | uint256 b; 50 | 51 | // assign values to parameters from calldata 52 | assembly { 53 | a := calldataload(0x01) 54 | b := calldataload(0x21) 55 | } 56 | 57 | // some large computation here 58 | uint256 result = a + b; 59 | 60 | // return result 61 | assembly { 62 | mstore(0x00, result) 63 | return(0x00, 0x20) 64 | } 65 | } 66 | 67 | function sub() internal { 68 | // returns bool success 69 | 70 | // declare paramters 71 | uint256 a; 72 | uint256 b; 73 | 74 | // assign values to parameters from calldata 75 | assembly { 76 | a := calldataload(0x01) 77 | b := calldataload(0x21) 78 | } 79 | 80 | // some large computation here 81 | uint256 result = a - b; 82 | 83 | // return result 84 | assembly { 85 | mstore(0x00, result) 86 | return(0x00, 0x20) 87 | } 88 | } 89 | 90 | function mul() internal { 91 | // returns bool success 92 | 93 | // declare paramters 94 | uint256 a; 95 | uint256 b; 96 | 97 | // assign values to parameters from calldata 98 | assembly { 99 | a := calldataload(0x01) 100 | b := calldataload(0x21) 101 | } 102 | 103 | // some large computation here 104 | uint256 result = a * b; 105 | 106 | // return result 107 | assembly { 108 | mstore(0x00, result) 109 | return(0x00, 0x20) 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/NonConstantGasExternalLibrary.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | contract NonConstantGasExternalLibrary { 5 | function add(uint256 a, uint256 b) external pure returns (uint256) { 6 | uint256 result = a + b; 7 | 8 | // return true 9 | assembly { 10 | mstore(0x00, result) 11 | return(0x00, 0x20) 12 | } 13 | } 14 | 15 | function sub(uint256 a, uint256 b) external pure returns (uint256) { 16 | uint256 result = a - b; 17 | 18 | // return true 19 | assembly { 20 | mstore(0x00, result) 21 | return(0x00, 0x20) 22 | } 23 | } 24 | 25 | function mul(uint256 a, uint256 b) external pure returns (uint256) { 26 | uint256 result = a * b; 27 | 28 | // return true 29 | assembly { 30 | mstore(0x00, result) 31 | return(0x00, 0x20) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/ConstantGasExternalLibrary.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import {Test, console2} from "forge-std/Test.sol"; 5 | import {ConstantGasExternalLibrary} from "../src/ConstantGasExternalLibrary.sol"; 6 | import {NonConstantGasExternalLibrary} from "../src/NonConstantGasExternalLibrary.sol"; 7 | 8 | contract ConstantGasExternalLibraryTest is Test { 9 | address public constantGas; 10 | address public nonConstantGas; 11 | uint8 ADD_JUMPDEST; 12 | uint8 SUB_JUMPDEST; 13 | uint8 MUL_JUMPDEST; 14 | 15 | function setUp() public { 16 | // get the jumpdests of the functions you need by passing in false which makes it revert with bytes of all the function's jumpdest abi.encoded 17 | // use try catch to prevent revert 18 | try new ConstantGasExternalLibrary(false) {} 19 | catch { 20 | uint8 mint_jumpdest; 21 | uint8 burn_jumpdest; 22 | uint8 transfer_jumpdest; 23 | 24 | // the returned jumpdests are 8 bytes, 4 bytes for the creation time jumpdest of the function and 4 bytes for the runtime jumpdest of the function 25 | // we only need the runtime jumpdest, so we cast it into a uint8 (1 byte). our contract is small so this is okay. 26 | assembly { 27 | let fmp := mload(0x40) 28 | returndatacopy(fmp, 0x00, 0x60) 29 | mint_jumpdest := mload(fmp) 30 | burn_jumpdest := mload(add(0x20, fmp)) 31 | transfer_jumpdest := mload(add(0x40, fmp)) 32 | mstore(0x40, add(fmp, 0x60)) 33 | } 34 | 35 | // store it in storage 36 | ADD_JUMPDEST = mint_jumpdest; 37 | SUB_JUMPDEST = burn_jumpdest; 38 | MUL_JUMPDEST = transfer_jumpdest; 39 | 40 | // deploy the contract, true makes the deployment succcessful 41 | constantGas = address(new ConstantGasExternalLibrary(true)); 42 | // console2.logBytes(abi.encodePacked(ADD_JUMPDEST, uint256(1000), uint256(2000))); 43 | // console2.logBytes(constantGas.code); 44 | } 45 | 46 | nonConstantGas = address(new NonConstantGasExternalLibrary()); 47 | 48 | // console2.log("Jumpdest below:"); 49 | // console2.logBytes1(bytes1(ADD_JUMPDEST)); 50 | // console2.logBytes1(bytes1(SUB_JUMPDEST)); 51 | // console2.logBytes1(bytes1(MUL_JUMPDEST)); 52 | } 53 | 54 | function test_ConstantGas() public { 55 | (bool success, bytes memory data) = 56 | constantGas.staticcall(abi.encodePacked(ADD_JUMPDEST, uint256(1000), uint256(2000))); 57 | assertEq(abi.decode(data, (uint256)), 3000); 58 | 59 | (success, data) = constantGas.staticcall(abi.encodePacked(SUB_JUMPDEST, uint256(2000), uint256(1000))); 60 | assertEq(abi.decode(data, (uint256)), 1000); 61 | 62 | (success, data) = constantGas.staticcall(abi.encodePacked(MUL_JUMPDEST, uint256(1000), uint256(2000))); 63 | assertEq(abi.decode(data, (uint256)), 2000000); 64 | } 65 | 66 | function test_NonConstantGas() public { 67 | (bool success, bytes memory data) = nonConstantGas.staticcall( 68 | abi.encodePacked(NonConstantGasExternalLibrary.add.selector, uint256(1000), uint256(2000)) 69 | ); 70 | assertEq(abi.decode(data, (uint256)), 3000); 71 | 72 | (success, data) = nonConstantGas.staticcall( 73 | abi.encodePacked(NonConstantGasExternalLibrary.sub.selector, uint256(2000), uint256(1000)) 74 | ); 75 | assertEq(abi.decode(data, (uint256)), 1000); 76 | 77 | (success, data) = nonConstantGas.staticcall( 78 | abi.encodePacked(NonConstantGasExternalLibrary.mul.selector, uint256(1000), uint256(2000)) 79 | ); 80 | assertEq(abi.decode(data, (uint256)), 2000000); 81 | } 82 | } 83 | --------------------------------------------------------------------------------