From d8697fe380b8ef17a4f85ebfe412abee2b930920 Mon Sep 17 00:00:00 2001 From: Saw-mon-and-Natalie Date: Tue, 14 Jun 2022 20:11:23 -0700 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20SSTORE2=20Gas=20Optimizations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gas-snapshot | 20 +++++------ package.json | 2 +- src/utils/SSTORE2.sol | 79 +++++++++++++++++++++++++++---------------- 3 files changed, 60 insertions(+), 41 deletions(-) diff --git a/.gas-snapshot b/.gas-snapshot index cb2d1653..21dfb52d 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -164,17 +164,17 @@ RolesAuthorityTest:testSetPublicCapabilities() (gas: 29183) RolesAuthorityTest:testSetRoleCapabilities() (gas: 30258) RolesAuthorityTest:testSetRoles() (gas: 28986) SSTORE2Test:testFailReadInvalidPointer() (gas: 2927) -SSTORE2Test:testFailReadInvalidPointerCustomBounds() (gas: 3099) +SSTORE2Test:testFailReadInvalidPointerCustomBounds() (gas: 3033) SSTORE2Test:testFailReadInvalidPointerCustomStartBound() (gas: 3004) -SSTORE2Test:testFailWriteReadEmptyOutOfBounds() (gas: 34470) -SSTORE2Test:testFailWriteReadOutOfBounds() (gas: 34426) -SSTORE2Test:testFailWriteReadOutOfStartBound() (gas: 34362) -SSTORE2Test:testWriteRead() (gas: 53497) -SSTORE2Test:testWriteReadCustomBounds() (gas: 34869) -SSTORE2Test:testWriteReadCustomStartBound() (gas: 34740) -SSTORE2Test:testWriteReadEmptyBound() (gas: 34677) -SSTORE2Test:testWriteReadFullBoundedRead() (gas: 53672) -SSTORE2Test:testWriteReadFullStartBound() (gas: 34764) +SSTORE2Test:testFailWriteReadEmptyOutOfBounds() (gas: 33784) +SSTORE2Test:testFailWriteReadOutOfBounds() (gas: 33740) +SSTORE2Test:testFailWriteReadOutOfStartBound() (gas: 33736) +SSTORE2Test:testWriteRead() (gas: 52536) +SSTORE2Test:testWriteReadCustomBounds() (gas: 34246) +SSTORE2Test:testWriteReadCustomStartBound() (gas: 34117) +SSTORE2Test:testWriteReadEmptyBound() (gas: 34054) +SSTORE2Test:testWriteReadFullBoundedRead() (gas: 52711) +SSTORE2Test:testWriteReadFullStartBound() (gas: 34141) SafeCastLibTest:testFailSafeCastTo128() (gas: 321) SafeCastLibTest:testFailSafeCastTo160() (gas: 342) SafeCastLibTest:testFailSafeCastTo192() (gas: 344) diff --git a/package.json b/package.json index b671cb2c..cfe4fc3b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@rari-capital/solmate", "license": "AGPL-3.0-only", - "version": "6.4.0", + "version": "6.5.0", "description": "Modern, opinionated and gas optimized building blocks for smart contract development.", "files": [ "src/**/*.sol" diff --git a/src/utils/SSTORE2.sol b/src/utils/SSTORE2.sol index bc10d72f..644458ae 100644 --- a/src/utils/SSTORE2.sol +++ b/src/utils/SSTORE2.sol @@ -5,42 +5,58 @@ pragma solidity >=0.8.0; /// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/utils/SSTORE2.sol) /// @author Modified from 0xSequence (https://github.com/0xSequence/sstore2/blob/master/contracts/SSTORE2.sol) library SSTORE2 { - uint256 internal constant DATA_OFFSET = 1; // We skip the first byte as it's a STOP opcode to ensure the contract can't be called. + error SSTORE2_DEPLOYMENT_FAILED(); + error SSTORE2_READ_OUT_OF_BOUNDS(); + + // We skip the first byte as it's a STOP opcode to ensure the contract can't be called. + uint256 internal constant DATA_OFFSET = 1; /*////////////////////////////////////////////////////////////// WRITE LOGIC //////////////////////////////////////////////////////////////*/ function write(bytes memory data) internal returns (address pointer) { - // Prefix the bytecode with a STOP opcode to ensure it cannot be called. - bytes memory runtimeCode = abi.encodePacked(hex"00", data); - - bytes memory creationCode = abi.encodePacked( - //---------------------------------------------------------------------------------------------------------------// - // Opcode | Opcode + Arguments | Description | Stack View // - //---------------------------------------------------------------------------------------------------------------// - // 0x60 | 0x600B | PUSH1 11 | codeOffset // - // 0x59 | 0x59 | MSIZE | 0 codeOffset // - // 0x81 | 0x81 | DUP2 | codeOffset 0 codeOffset // - // 0x38 | 0x38 | CODESIZE | codeSize codeOffset 0 codeOffset // - // 0x03 | 0x03 | SUB | (codeSize - codeOffset) 0 codeOffset // - // 0x80 | 0x80 | DUP | (codeSize - codeOffset) (codeSize - codeOffset) 0 codeOffset // - // 0x92 | 0x92 | SWAP3 | codeOffset (codeSize - codeOffset) 0 (codeSize - codeOffset) // - // 0x59 | 0x59 | MSIZE | 0 codeOffset (codeSize - codeOffset) 0 (codeSize - codeOffset) // - // 0x39 | 0x39 | CODECOPY | 0 (codeSize - codeOffset) // - // 0xf3 | 0xf3 | RETURN | // - //---------------------------------------------------------------------------------------------------------------// - hex"60_0B_59_81_38_03_80_92_59_39_F3", // Returns all code in the contract except for the first 11 (0B in hex) bytes. - runtimeCode // The bytecode we want the contract to have after deployment. Capped at 1 byte less than the code size limit. - ); - + // Note: The assembly block below does not expand the memory. assembly { + let originalDataLength := mload(data) + + // Add 1 to data size since we are prefixing it with a STOP opcode. + let dataSize := add(originalDataLength, 1) + + /** + * ------------------------------------------------------------------------------------+ + * Opcode | Opcode + Arguments | Description | Stack View | + * ------------------------------------------------------------------------------------| + * 0x61 | 0x61XXXX | PUSH2 codeSize | codeSize | + * 0x80 | 0x80 | DUP1 | codeSize codeSize | + * 0x60 | 0x600A | PUSH1 10 | 10 codeSize codeSize | + * 0x3D | 0x3D | RETURNDATASIZE | 0 10 codeSize codeSize | + * 0x39 | 0x39 | CODECOPY | codeSize | + * 0x3D | 0x3D | RETURNDATASZIE | 0 codeSize | + * 0xF3 | 0xF3 | RETURN | | + * 0x00 | 0x00 | STOP | | + * ------------------------------------------------------------------------------------+ + * @dev Prefix the bytecode with a STOP opcode to ensure it cannot be called. Also PUSH2 is + * used since max contract size cap is 24,576 bytes which is less than 2 ** 16. + */ + mstore( + data, + or( + 0x61000080600a3d393df300, + shl(64, dataSize) // shift `dataSize` so that it lines up with the 0000 after PUSH2 + ) + ) + // Deploy a new contract with the generated creation code. - // We start 32 bytes into the code to avoid copying the byte length. - pointer := create(0, add(creationCode, 32), mload(creationCode)) + pointer := create(0, add(data, 21), add(dataSize, 10)) + + // Restore original length of the variable size `data` + mstore(data, originalDataLength) } - require(pointer != address(0), "DEPLOYMENT_FAILED"); + if (pointer == address(0)) { + revert SSTORE2_DEPLOYMENT_FAILED(); + } } /*////////////////////////////////////////////////////////////// @@ -65,7 +81,9 @@ library SSTORE2 { start += DATA_OFFSET; end += DATA_OFFSET; - require(pointer.code.length >= end, "OUT_OF_BOUNDS"); + if (pointer.code.length < end) { + revert SSTORE2_READ_OUT_OF_BOUNDS(); + } return readBytecode(pointer, start, end - start); } @@ -85,9 +103,10 @@ library SSTORE2 { // Update the free memory pointer to prevent overriding our data. // We use and(x, not(31)) as a cheaper equivalent to sub(x, mod(x, 32)). - // Adding 31 to size and running the result through the logic above ensures - // the memory pointer remains word-aligned, following the Solidity convention. - mstore(0x40, add(data, and(add(add(size, 32), 31), not(31)))) + // Adding 63 (32 + 31) to size and running the result through the logic + // above ensures the memory pointer remains word-aligned, following + // the Solidity convention. + mstore(0x40, add(data, and(add(size, 63), not(31)))) // Store the size of the data in the first 32 byte chunk of free memory. mstore(data, size)