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 all 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
70 changes: 53 additions & 17 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,10 @@ 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.
// SSTORE2 deployed storage contracts take the general format of:
// concat(0x00, data)
// note: this is true for both variants of the SSTORE2 library
uint256 internal constant SSTORE2_DATA_OFFSET = 1;
// V0 deployed storage contracts take the general format of:
// concat(gated-cleanup-logic, deployer-address, data)
uint256 internal constant V0_ADDRESS_OFFSET = 72;
Expand Down Expand Up @@ -148,27 +153,29 @@ library BytecodeStorage {
//////////////////////////////////////////////////////////////*/

/**
* @notice Read a string from contract bytecode
* @notice Read the bytes from contract bytecode, with an explicitly provided starting offset
* @param _address address of deployed contract with bytecode stored in the V0 or V1 format
* @return data string read from contract bytecode
* @param _offset offset to read from in contract bytecode, explicitly provided (not calculated)
* @return data bytes 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
) internal view returns (string memory data) {
function readBytesFromBytecode(
address _address,
uint256 _offset
) internal view returns (bytes 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
if (bytecodeSize < dataOffset) {
// handle case where address contains code < _offset
if (bytecodeSize < _offset) {
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 @@ -182,10 +189,36 @@ 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 the bytes from contract bytecode that was written to the EVM using SSTORE2
* @param _address address of deployed contract with bytecode stored in the SSTORE2 format
* @return data bytes 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 readBytesFromSSTORE2Bytecode(
address _address
) internal view returns (bytes memory data) {
return readBytesFromBytecode(_address, SSTORE2_DATA_OFFSET);
}

/**
* @notice Read a string from contract bytecode
* @param _address address of deployed contract with bytecode stored in the V0 or V1 format
* @return data string read from contract bytecode
* @dev This function performs input validation that the contract to read is in an expected format
*/
function readFromBytecode(
address _address
) internal view returns (string memory data) {
uint256 dataOffset = _bytecodeDataOffsetAt(_address);
return string(readBytesFromBytecode(_address, dataOffset));
}

/**
* @notice Get address for deployer for given contract bytecode
* @param _address address of deployed contract with bytecode stored in the V0 or V1 format
Expand Down Expand Up @@ -271,7 +304,8 @@ library BytecodeStorage {
} else if (version == V0_VERSION_STRING) {
dataOffset = V0_DATA_OFFSET;
} else {
dataOffset = 0;
// unknown version, revert
revert("ContractAsStorage: Unsupported Version");
}
}

Expand All @@ -289,7 +323,8 @@ library BytecodeStorage {
} else if (version == V0_VERSION_STRING) {
addressOffset = V0_ADDRESS_OFFSET;
} else {
addressOffset = 0;
// unknown version, revert
revert("ContractAsStorage: Unsupported Version");
}
}

Expand All @@ -303,9 +338,10 @@ library BytecodeStorage {
) private view returns (bytes32 version) {
// get the size of the data
uint256 bytecodeSize = _bytecodeSizeAt(_address);
// handle case where address contains code < minimum expected version string size
// handle case where address contains code < minimum expected version string size,
// by returning early with the unknown version string
if (bytecodeSize < (VERSION_OFFSET + 32)) {
revert("ContractAsStorage: Read Error");
return UNKNOWN_VERSION_STRING;
jakerockland marked this conversation as resolved.
Show resolved Hide resolved
}

assembly {
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)
}
}
}
33 changes: 33 additions & 0 deletions contracts/mock/BytecodeV1TextCR_DMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,39 @@ 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 string(_bytecodeAddress.readBytesFromBytecode(_offset));
}

/**
* @notice Allows additional read introspection, to read a chunk of text,
* from chain-state that lives at a given deployed address that
* was written with SSTORE2.
* @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 readSSTORE2TextAtAddress(
address _bytecodeAddress
) public view returns (string memory) {
return string(_bytecodeAddress.readBytesFromSSTORE2Bytecode());
}

/**
* @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