Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

STORE2-compatible reads in BytecodeStorage #681

Merged
merged 5 commits into from
May 2, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 41 additions & 12 deletions contracts/libs/0.8.x/BytecodeStorageV1.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ pragma solidity ^0.8.0;
* @notice Utilize contract bytecode as persistant storage for large chunks of script string data.
* This library is intended to have an external deployed copy that is released in the future,
* and, as such, has been designed to support both updated V1 (versioned, with purging removed)
* reads as well as backwards-compatible reads for the unversioned "V0" storage contracts
* which were deployed by the original version of this libary.
* reads as well as backwards-compatible reads for both a) the unversioned "V0" storage contracts
* which were deployed by the original version of this libary and b) contracts that were deployed
* using one of the SSTORE2 implementations referenced below.
* For these pre-V1 storage contracts (which themselves did not have any explict versioning semantics)
* backwards-compatible reads are optimistic, and only expected to work for contracts actually
* deployed by the original version of this library – and may fail ungracefully if attempted to be
Expand Down Expand Up @@ -49,6 +50,7 @@ library BytecodeStorage {
uint256 internal constant DATA_OFFSET = 65;

// Define the set of known *historic* offset values for where the "meta bytes" end, and the "data bytes" begin.
uint256 internal constant SSTORE2_FALLBACK_DATA_OFFSET = 1;
uint256 internal constant V0_ADDRESS_OFFSET = 72;
uint256 internal constant V0_DATA_OFFSET = 104;
uint256 internal constant V1_ADDRESS_OFFSET = ADDRESS_OFFSET;
Expand Down Expand Up @@ -142,31 +144,33 @@ library BytecodeStorage {
//////////////////////////////////////////////////////////////*/

/**
* @notice Read a string from contract bytecode
* @param _address address of deployed contract with bytecode containing concat(invalid opcode, version, deployer-address, data)
* @notice Read a string from contract bytecode, with an explicitly provided offset
* @param _address address of deployed contract to read from
* @param _offset offset to read from in contract bytecode, explicitly provided (not calculated)
* @return data string read from contract bytecode
* @dev This function performs no input validation on the provided contract,
* other than that there is content to read (but not that its a "storage contract")
*/
function readFromBytecode(
address _address
function forceReadFromBytecode(
address _address,
uint256 _offset
) internal view returns (string memory data) {
// get the size of the bytecode
uint256 bytecodeSize = _bytecodeSizeAt(_address);
// the dataOffset for the bytecode
uint256 dataOffset = _bytecodeDataOffsetAt(_address);
// handle case where address contains code < dataOffset
// note: the first check here also captures the case where
// (bytecodeSize == 0) implicitly, but we add the second check of
// (bytecodeSize == 0) as a fall-through that will never execute
// unless `dataOffset` is set to 0 at some point.
if ((bytecodeSize < dataOffset) || (bytecodeSize == 0)) {
if ((bytecodeSize < _offset) || (bytecodeSize == 0)) {
revert("ContractAsStorage: Read Error");
}

// handle case where address contains code >= dataOffset
// decrement by dataOffset to account for header info
uint256 size;
unchecked {
size = bytecodeSize - dataOffset;
size = bytecodeSize - _offset;
}

assembly {
Expand All @@ -180,8 +184,27 @@ library BytecodeStorage {
// store length of data in first 32 bytes
mstore(data, size)
// copy code to memory, excluding the deployer-address
extcodecopy(_address, add(data, 0x20), dataOffset, size)
extcodecopy(_address, add(data, 0x20), _offset, size)
}
}

/**
* @notice Read a string from contract bytecode
* @param _address address of deployed contract with bytecode containing concat(invalid opcode, version, deployer-address, data)
* @return data string read from contract bytecode
*/
function readFromBytecode(
address _address
) internal view returns (string memory data) {
// the dataOffset for the bytecode
uint256 dataOffset = _bytecodeDataOffsetAt(_address);
// validate that version is readable
// note: never expected to throw for reads, as we
// fall back to SSTORE-2 reads optimistically
jakerockland marked this conversation as resolved.
Show resolved Hide resolved
if (dataOffset == 0) {
revert("ContractAsStorage: Unsupported Version");
}
return forceReadFromBytecode(_address, dataOffset);
}

/**
Expand All @@ -196,6 +219,10 @@ library BytecodeStorage {
uint256 bytecodeSize = _bytecodeSizeAt(_address);
// the dataOffset for the bytecode
uint256 addressOffset = _bytecodeAddressOffsetAt(_address);
// validate that version is readable
if (addressOffset == 0) {
revert("ContractAsStorage: Unsupported Version");
}
jakerockland marked this conversation as resolved.
Show resolved Hide resolved
// handle case where address contains code < addressOffset
// note: the first check here also captures the case where
// (bytecodeSize == 0) implicitly, but we add the second check of
Expand Down Expand Up @@ -270,7 +297,8 @@ library BytecodeStorage {
} else if (version == V0_VERSION_STRING) {
dataOffset = V0_DATA_OFFSET;
} else {
dataOffset = 0;
// unknown version, fallback to attempting an SSTORE2-compatible read
dataOffset = SSTORE2_FALLBACK_DATA_OFFSET;
}
}

Expand All @@ -288,6 +316,7 @@ library BytecodeStorage {
} else if (version == V0_VERSION_STRING) {
addressOffset = V0_ADDRESS_OFFSET;
} else {
// unknown version, support for deploying-address reads is not expected
addressOffset = 0;
}
}
Expand Down
109 changes: 109 additions & 0 deletions contracts/libs/0.8.x/SSTORE2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity >=0.8.0;

/// @notice Read and write to persistent storage at a fraction of the cost.
/// @author Solmate (https://github.com/transmissions11/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.

/*//////////////////////////////////////////////////////////////
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.
);

/// @solidity memory-safe-assembly
assembly {
// 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))
}

require(pointer != address(0), "DEPLOYMENT_FAILED");
}

/*//////////////////////////////////////////////////////////////
READ LOGIC
//////////////////////////////////////////////////////////////*/

function read(address pointer) internal view returns (bytes memory) {
return
readBytecode(
pointer,
DATA_OFFSET,
pointer.code.length - DATA_OFFSET
);
}

function read(
address pointer,
uint256 start
) internal view returns (bytes memory) {
start += DATA_OFFSET;

return readBytecode(pointer, start, pointer.code.length - start);
}

function read(
address pointer,
uint256 start,
uint256 end
) internal view returns (bytes memory) {
start += DATA_OFFSET;
end += DATA_OFFSET;

require(pointer.code.length >= end, "OUT_OF_BOUNDS");

return readBytecode(pointer, start, end - start);
}

/*//////////////////////////////////////////////////////////////
INTERNAL HELPER LOGIC
//////////////////////////////////////////////////////////////*/

function readBytecode(
address pointer,
uint256 start,
uint256 size
) private view returns (bytes memory data) {
/// @solidity memory-safe-assembly
assembly {
// Get a pointer to some free memory.
data := mload(0x40)

// 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))))

// Store the size of the data in the first 32 byte chunk of free memory.
mstore(data, size)

// Copy the code into memory right after the 32 bytes we used to store the size.
extcodecopy(pointer, add(data, 32), start, size)
}
}
}
18 changes: 18 additions & 0 deletions contracts/mock/BytecodeV1TextCR_DMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,24 @@ contract BytecodeV1TextCR_DMock {
return _bytecodeAddress.readFromBytecode();
}

/**
* @notice Allows additional read introspection, to read a chunk of text,
* from chain-state that lives at a given deployed address with an
* explicitly provided `_offset`.
* @param _bytecodeAddress address from which to read text content.
* @param _offset Offset to read from in contract bytecode,
* explicitly provided (not calculated)
* @return string Content read from contract bytecode at the given address.
* @dev Intentionally do not perform input validation, instead allowing
* the underlying BytecodeStorage lib to throw errors where applicable.
*/
function forceReadTextAtAddress(
address _bytecodeAddress,
uint256 _offset
) public view returns (string memory) {
return _bytecodeAddress.forceReadFromBytecode(_offset);
}

/**
* @notice Allows introspection of who deployed a given contracts-as-storage
* contract, based on a provided `_bytecodeAddress`.
Expand Down
93 changes: 93 additions & 0 deletions contracts/mock/SSTORE2Mock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity 0.8.17;

// Created By: Art Blocks Inc.

import "../libs/0.8.x/SSTORE2.sol";

/**
* @title Art Blocks SSTORE2Mock.
* @author Art Blocks Inc.
* @notice This contract serves as a mock client of the SSTORE2 library
* to allow for more testing of this library in the context of
* backwards-compatible reads with the Art Blocks BytecodeStorage library
* @dev For the purposes of our backwards-compatibility testing, the two different
* variations of the SSTORE2 library are functionally equivalent, so we just test
* against the one that is more widely tracked on Github for simplicity, but the
* same tests could be run against the other variation of the library as well
* and would be expected to "just work" given both use the same data offset of
* a single 0x00 "stop byte".
*/
contract SSTORE2Mock {
using SSTORE2 for bytes;
using SSTORE2 for address;

// monotonically increasing slot counter and associated slot-storage mapping
uint256 public nextTextSlotId = 0;
mapping(uint256 => address) public storedTextBytecodeAddresses;

// save deployer address to support basic ACL checks for non-read operations
address public deployerAddress;

modifier onlyDeployer() {
require(msg.sender == deployerAddress, "Only deployer");
_;
}

/**
* @notice Initializes contract.
*/
constructor() {
deployerAddress = msg.sender;
}

/*//////////////////////////////////////////////////////////////
Create + Read
//////////////////////////////////////////////////////////////*/

/**
* @notice "Create": Adds a chunk of text to be stored to chain-state.
* @param _text Text to be created in chain-state.
* @return uint256 Slot that the written bytecode contract address was
* stored in.
* @dev Intentionally do not perform input validation, instead allowing
* the underlying BytecodeStorage lib to throw errors where applicable.
*/
function createText(
string memory _text
) external onlyDeployer returns (uint256) {
// store text in contract bytecode
storedTextBytecodeAddresses[nextTextSlotId] = bytes(_text).write();
// record written slot before incrementing
uint256 textSlotId = nextTextSlotId;
nextTextSlotId++;
return textSlotId;
}

/**
* @notice "Read": Reads chunk of text currently in the provided slot, from
* chain-state.
* @param _textSlotId Slot (associated with this contract) for which to
* read text content.
* @return string Content read from contract bytecode in the given slot.
* @dev Intentionally do not perform input validation, instead allowing
* the underlying BytecodeStorage lib to throw errors where applicable.
*/
function readText(uint256 _textSlotId) public view returns (string memory) {
return string(storedTextBytecodeAddresses[_textSlotId].read());
}

/**
* @notice Allows additional read introspection, to read a chunk of text,
* from chain-state that lives at a given deployed address.
* @param _bytecodeAddress address from which to read text content.
* @return string Content read from contract bytecode at the given address.
* @dev Intentionally do not perform input validation, instead allowing
* the underlying BytecodeStorage lib to throw errors where applicable.
*/
function readTextAtAddress(
address _bytecodeAddress
) public view returns (string memory) {
return string(_bytecodeAddress.read());
}
}
Loading