// SPDX-License-Identifier: MITpragmasolidity >=0.8.17 .0;import {EventsAndErrors} from"./EventsAndErrors.sol";
import {INet} from"./INet.sol";
import {SSTORE2} from"@solady/utils/SSTORE2.sol";
/// @title Net/// @author Aspyn Palatnick (aspyn.eth, stuckinaboot.eth)/// @notice Fully decentralized onchain messaging protocolcontractNetisINet, EventsAndErrors{
// Use a single global mapping to map hashes to message indexesmapping(bytes32 hashVal =>uint256[] messageIndexes)
public hashToMessageIndexes;
address[] public messagePointers;
bytes32constant ZERO_HASH =keccak256(abi.encodePacked(address(0)));
// Empty topic "" will not impact a hash, which could result in collisions// between hash values that use topic and don't use topic. For that reason,// we prefix the relevant hash topic keys with these values to ensure collisions don't occur// Example if this prefix didn't exist:// keccak256(abi.encodePacked(address(0))) == keccak256(abi.encodePacked(address(0), "" /* where "" represents topic */)) evaluates to trueuint256constant APP_TOPIC_HASH_PREFIX =1;
uint256constant APP_USER_TOPIC_HASH_PREFIX =2;
// ************// Send message// ************/// @notice Send message via app/// @param sender message sender/// @param text message text/// @param topic message topic/// @param data message datafunctionsendMessageViaApp(address sender,
stringcalldata text,
stringcalldata topic,
bytescalldata data
) external{
// Revert if message length is none to prevent empty messagesif (bytes(text).length==0&&bytes(data).length==0) {
revert MsgEmpty();
}
// Track message index in topic and user mappingsuint256 messagesLength = messagePointers.length;
// App messages
hashToMessageIndexes[keccak256(abi.encodePacked(msg.sender))].push(
messagesLength
);
// App-user messages
hashToMessageIndexes[keccak256(abi.encodePacked(msg.sender, sender))]
.push(messagesLength);
// App-topic messages
hashToMessageIndexes[
// msg.sender is the app idkeccak256(
abi.encodePacked(APP_TOPIC_HASH_PREFIX, msg.sender, topic)
)
].push(messagesLength);
// App-user-topic messages
hashToMessageIndexes[
keccak256(
abi.encodePacked(
APP_USER_TOPIC_HASH_PREFIX,
msg.sender,
sender,
topic
)
)
].push(messagesLength);
// Emit message sent using current messages length as the indexemit MessageSentViaApp(msg.sender, sender, topic, messagesLength);
// Store message
messagePointers.push(
SSTORE2.write(
abi.encode(
// Appmsg.sender,
// Sender
sender,
// Timestampblock.timestamp,
// Data
data,
// Text
text,
// Topic
topic
)
)
);
}
/// @notice Send message/// @param text message text/// @param topic message topic/// @param data message datafunctionsendMessage(stringcalldata text,
stringcalldata topic,
bytescalldata data
) external{
// Revert if message length is none to prevent empty messagesif (bytes(text).length==0&&bytes(data).length==0) {
revert MsgEmpty();
}
// Track message index in topic and user mappingsuint256 messagesLength = messagePointers.length;
// address(0) is used to represent messages sent from "no app"
hashToMessageIndexes[ZERO_HASH].push(messagesLength);
hashToMessageIndexes[
keccak256(
abi.encodePacked(APP_TOPIC_HASH_PREFIX, address(0), topic)
)
].push(messagesLength);
hashToMessageIndexes[
keccak256(abi.encodePacked(address(0), msg.sender))
].push(messagesLength);
hashToMessageIndexes[
keccak256(
abi.encodePacked(
APP_USER_TOPIC_HASH_PREFIX,
address(0),
msg.sender,
topic
)
)
].push(messagesLength);
// Emit message sent using current messages length as the indexemit MessageSent(msg.sender, topic, messagesLength);
// Store message
messagePointers.push(
SSTORE2.write(
abi.encode(
// Appaddress(0),
// Sendermsg.sender,
// Timestampblock.timestamp,
// Data
data,
// Text
text,
// Topic
topic
)
)
);
}
// **************// Fetch Messages// **************// Fetch message indexes/// @notice Get message pointer index for app/// @param idx message index/// @param app app/// @return index indexfunctiongetMessageIdxForApp(uint256 idx,
address app
) externalviewreturns (uint256) {
return hashToMessageIndexes[keccak256(abi.encodePacked(app))][idx];
}
/// @notice Get message pointer index for app user/// @param idx message index/// @param app app/// @param user user/// @return index indexfunctiongetMessageIdxForAppUser(uint256 idx,
address app,
address user
) externalviewreturns (uint256) {
return
hashToMessageIndexes[keccak256(abi.encodePacked(app, user))][idx];
}
/// @notice Get message pointer index for app topic/// @param idx message index/// @param app app/// @param topic topic/// @return index indexfunctiongetMessageIdxForAppTopic(uint256 idx,
address app,
stringcalldata topic
) externalviewreturns (uint256) {
return
hashToMessageIndexes[
keccak256(abi.encodePacked(APP_TOPIC_HASH_PREFIX, app, topic))
][idx];
}
/// @notice Get message pointer index for app user topic/// @param idx message index/// @param app app/// @param user user/// @param topic topic/// @return index indexfunctiongetMessageIdxForAppUserTopic(uint256 idx,
address app,
address user,
stringcalldata topic
) externalviewreturns (uint256) {
return
hashToMessageIndexes[
keccak256(
abi.encodePacked(
APP_USER_TOPIC_HASH_PREFIX,
app,
user,
topic
)
)
][idx];
}
// Fetch single message/// @notice Decode encoded message/// @param encodedMessage encoded message/// @return decodedMessage decoded messagefunctiondecodeMessage(bytesmemory encodedMessage
) publicpurereturns (Message memory) {
Message memory message;
(
message.app,
message.sender,
message.timestamp,
message.data,
message.text,
message.topic
) =abi.decode(
encodedMessage,
(
// Appaddress,
// Senderaddress,
// Timestampuint256,
// Databytes,
// Textstring,
// Topicstring
)
);
return message;
}
/// @notice Decode message at index in message pointers/// @param idx index/// @return decodedMessage decoded messagefunctiondecodeMessageAtIndex(uint256 idx
) publicviewreturns (Message memory) {
return decodeMessage(SSTORE2.read(messagePointers[idx]));
}
/// @notice Get message/// @param idx index/// @return message messagefunctiongetMessage(uint256 idx) externalviewreturns (Message memory) {
return decodeMessageAtIndex(idx);
}
/// @notice Get message for app/// @param idx index/// @param app app/// @return message messagefunctiongetMessageForApp(uint256 idx,
address app
) externalviewreturns (Message memory) {
return
decodeMessageAtIndex(
hashToMessageIndexes[keccak256(abi.encodePacked(app))][idx]
);
}
/// @notice Get message for app user/// @param idx index/// @param app app/// @param user user/// @return message messagefunctiongetMessageForAppUser(uint256 idx,
address app,
address user
) externalviewreturns (Message memory) {
return
decodeMessageAtIndex(
hashToMessageIndexes[keccak256(abi.encodePacked(app, user))][
idx
]
);
}
/// @notice Get message for app topic/// @param idx index/// @param app app/// @param topic topic/// @return message messagefunctiongetMessageForAppTopic(uint256 idx,
address app,
stringcalldata topic
) externalviewreturns (Message memory) {
return
decodeMessageAtIndex(
hashToMessageIndexes[
keccak256(
abi.encodePacked(APP_TOPIC_HASH_PREFIX, app, topic)
)
][idx]
);
}
/// @notice Get message for app user topic/// @param idx index/// @param app app/// @param user user/// @param topic topic/// @return message messagefunctiongetMessageForAppUserTopic(uint256 idx,
address app,
address user,
stringcalldata topic
) externalviewreturns (Message memory) {
return
decodeMessageAtIndex(
hashToMessageIndexes[
keccak256(
abi.encodePacked(
APP_USER_TOPIC_HASH_PREFIX,
app,
user,
topic
)
)
][idx]
);
}
// Fetch multiple messages/// @notice Get messages in range/// @param startIdx start index/// @param endIdx end index/// @return messages list of messagesfunctiongetMessagesInRange(uint256 startIdx,
uint256 endIdx
) externalviewreturns (Message[] memory) {
if (startIdx >= endIdx) {
revert InvalidRange();
}
uint256 querySetLength = messagePointers.length;
if (startIdx +1> querySetLength) {
revert InvalidStartIndex();
}
if (endIdx > querySetLength) {
revert InvalidEndIndex();
}
Message[] memory messagesSlice =new Message[](endIdx - startIdx);
uint256 idxInMessages = startIdx;
unchecked {
for (; idxInMessages < endIdx; ) {
messagesSlice[idxInMessages - startIdx] = decodeMessageAtIndex(
idxInMessages
);
++idxInMessages;
}
}
return messagesSlice;
}
/// @notice Get messages in range for hash/// @param startIdx start index/// @param endIdx end index/// @param hashVal hash/// @return messages list of messagesfunctiongetMessagesInRangeForHash(uint256 startIdx,
uint256 endIdx,
bytes32 hashVal
) publicviewreturns (Message[] memory) {
if (startIdx >= endIdx) {
revert InvalidRange();
}
uint256 querySetLength = hashToMessageIndexes[hashVal].length;
if (startIdx +1> querySetLength) {
revert InvalidStartIndex();
}
if (endIdx > querySetLength) {
revert InvalidEndIndex();
}
Message[] memory messagesSlice =new Message[](endIdx - startIdx);
uint256 idxInMessages = startIdx;
unchecked {
for (; idxInMessages < endIdx; ) {
messagesSlice[idxInMessages - startIdx] = decodeMessageAtIndex(
hashToMessageIndexes[hashVal][idxInMessages]
);
++idxInMessages;
}
}
return messagesSlice;
}
/// @notice Get messages in range for app/// @param startIdx start index/// @param endIdx end index/// @param app app/// @return messages list of messagesfunctiongetMessagesInRangeForApp(uint256 startIdx,
uint256 endIdx,
address app
) externalviewreturns (Message[] memory) {
return
getMessagesInRangeForHash(
startIdx,
endIdx,
keccak256(abi.encodePacked(app))
);
}
/// @notice Get messages in range for app user/// @param startIdx start index/// @param endIdx end index/// @param app app/// @param user user/// @return messages list of messagesfunctiongetMessagesInRangeForAppUser(uint256 startIdx,
uint256 endIdx,
address app,
address user
) externalviewreturns (Message[] memory) {
return
getMessagesInRangeForHash(
startIdx,
endIdx,
keccak256(abi.encodePacked(app, user))
);
}
/// @notice Get messages in range for app topic/// @param startIdx start index/// @param endIdx end index/// @param app app/// @param topic topic/// @return messages list of messagesfunctiongetMessagesInRangeForAppTopic(uint256 startIdx,
uint256 endIdx,
address app,
stringcalldata topic
) externalviewreturns (Message[] memory) {
return
getMessagesInRangeForHash(
startIdx,
endIdx,
keccak256(abi.encodePacked(APP_TOPIC_HASH_PREFIX, app, topic))
);
}
/// @notice Get messages in range for app user topic/// @param startIdx start index/// @param endIdx end index/// @param app app/// @param user user/// @param topic topic/// @return messages list of messagesfunctiongetMessagesInRangeForAppUserTopic(uint256 startIdx,
uint256 endIdx,
address app,
address user,
stringcalldata topic
) externalviewreturns (Message[] memory) {
return
getMessagesInRangeForHash(
startIdx,
endIdx,
keccak256(
abi.encodePacked(
APP_USER_TOPIC_HASH_PREFIX,
app,
user,
topic
)
)
);
}
// **************// Message counts// **************/// @notice Get total messages count/// @return count countfunctiongetTotalMessagesCount() externalviewreturns (uint256) {
return messagePointers.length;
}
/// @notice Get total messages for hash count/// @param hashVal hash/// @return count countfunctiongetTotalMessagesForHashCount(bytes32 hashVal
) publicviewreturns (uint256) {
return hashToMessageIndexes[hashVal].length;
}
/// @notice Get total messages for app count/// @param app app/// @return count countfunctiongetTotalMessagesForAppCount(address app
) externalviewreturns (uint256) {
return getTotalMessagesForHashCount(keccak256(abi.encodePacked(app)));
}
/// @notice Get total messages for app user count/// @param app app/// @param user user/// @return count countfunctiongetTotalMessagesForAppUserCount(address app,
address user
) externalviewreturns (uint256) {
return
getTotalMessagesForHashCount(
keccak256(abi.encodePacked(app, user))
);
}
/// @notice Get total messages for app topic count/// @param app app/// @param topic topic/// @return count countfunctiongetTotalMessagesForAppTopicCount(address app,
stringcalldata topic
) externalviewreturns (uint256) {
return
getTotalMessagesForHashCount(
keccak256(abi.encodePacked(APP_TOPIC_HASH_PREFIX, app, topic))
);
}
/// @notice Get total messages for app user topic count/// @param app app/// @param user user/// @param topic topic/// @return count countfunctiongetTotalMessagesForAppUserTopicCount(address app,
address user,
stringcalldata topic
) externalviewreturns (uint256) {
return
getTotalMessagesForHashCount(
keccak256(
abi.encodePacked(
APP_USER_TOPIC_HASH_PREFIX,
app,
user,
topic
)
)
);
}
}
Contract Source Code
File 4 of 4: SSTORE2.sol
// SPDX-License-Identifier: MITpragmasolidity ^0.8.4;/// @notice Read and write to persistent storage at a fraction of the cost./// @author Solady (https://github.com/vectorized/solmady/blob/main/src/utils/SSTORE2.sol)/// @author Saw-mon-and-Natalie (https://github.com/Saw-mon-and-Natalie)/// @author Modified from 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)librarySSTORE2{
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*//* CONSTANTS *//*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*//// @dev We skip the first byte as it's a STOP opcode,/// which ensures the contract can't be called.uint256internalconstant DATA_OFFSET =1;
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*//* CUSTOM ERRORS *//*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*//// @dev Unable to deploy the storage contract.errorDeploymentFailed();
/// @dev The storage contract address is invalid.errorInvalidPointer();
/// @dev Attempt to read outside of the storage contract's bytecode bounds.errorReadOutOfBounds();
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*//* WRITE LOGIC *//*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*//// @dev Writes `data` into the bytecode of a storage contract and returns its address.functionwrite(bytesmemory data) internalreturns (address pointer) {
/// @solidity memory-safe-assemblyassembly {
let originalDataLength :=mload(data)
// Add 1 to data size since we are prefixing it with a STOP opcode.let dataSize :=add(originalDataLength, DATA_OFFSET)
/**
* ------------------------------------------------------------------------------+
* Opcode | Mnemonic | Stack | Memory |
* ------------------------------------------------------------------------------|
* 61 dataSize | PUSH2 dataSize | dataSize | |
* 80 | DUP1 | dataSize dataSize | |
* 60 0xa | PUSH1 0xa | 0xa dataSize dataSize | |
* 3D | RETURNDATASIZE | 0 0xa dataSize dataSize | |
* 39 | CODECOPY | dataSize | [0..dataSize): code |
* 3D | RETURNDATASIZE | 0 dataSize | [0..dataSize): code |
* F3 | RETURN | | [0..dataSize): code |
* 00 | 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(
// Do a out-of-gas revert if `dataSize` is more than 2 bytes.// The actual EVM limit may be smaller and may change over time.add(data, gt(dataSize, 0xffff)),
// Left shift `dataSize` by 64 so that it lines up with the 0000 after PUSH2.or(0xfd61000080600a3d393df300, shl(0x40, dataSize))
)
// Deploy a new contract with the generated creation code.
pointer :=create(0, add(data, 0x15), add(dataSize, 0xa))
// If `pointer` is zero, revert.ifiszero(pointer) {
// Store the function selector of `DeploymentFailed()`.mstore(0x00, 0x30116425)
// Revert with (offset, size).revert(0x1c, 0x04)
}
// Restore original length of the variable size `data`.mstore(data, originalDataLength)
}
}
/// @dev Writes `data` into the bytecode of a storage contract with `salt`/// and returns its deterministic address.functionwriteDeterministic(bytesmemory data, bytes32 salt)
internalreturns (address pointer)
{
/// @solidity memory-safe-assemblyassembly {
let originalDataLength :=mload(data)
let dataSize :=add(originalDataLength, DATA_OFFSET)
mstore(
// Do a out-of-gas revert if `dataSize` is more than 2 bytes.// The actual EVM limit may be smaller and may change over time.add(data, gt(dataSize, 0xffff)),
// Left shift `dataSize` by 64 so that it lines up with the 0000 after PUSH2.or(0xfd61000080600a3d393df300, shl(0x40, dataSize))
)
// Deploy a new contract with the generated creation code.
pointer :=create2(0, add(data, 0x15), add(dataSize, 0xa), salt)
// If `pointer` is zero, revert.ifiszero(pointer) {
// Store the function selector of `DeploymentFailed()`.mstore(0x00, 0x30116425)
// Revert with (offset, size).revert(0x1c, 0x04)
}
// Restore original length of the variable size `data`.mstore(data, originalDataLength)
}
}
/// @dev Returns the initialization code hash of the storage contract for `data`./// Used for mining vanity addresses with create2crunch.functioninitCodeHash(bytesmemory data) internalpurereturns (bytes32 hash) {
/// @solidity memory-safe-assemblyassembly {
let originalDataLength :=mload(data)
let dataSize :=add(originalDataLength, DATA_OFFSET)
// Do a out-of-gas revert if `dataSize` is more than 2 bytes.// The actual EVM limit may be smaller and may change over time.returndatacopy(returndatasize(), returndatasize(), shr(16, dataSize))
mstore(data, or(0x61000080600a3d393df300, shl(0x40, dataSize)))
hash :=keccak256(add(data, 0x15), add(dataSize, 0xa))
// Restore original length of the variable size `data`.mstore(data, originalDataLength)
}
}
/// @dev Returns the address of the storage contract for `data`/// deployed with `salt` by `deployer`./// Note: The returned result has dirty upper 96 bits. Please clean if used in assembly.functionpredictDeterministicAddress(bytesmemory data, bytes32 salt, address deployer)
internalpurereturns (address predicted)
{
bytes32 hash = initCodeHash(data);
/// @solidity memory-safe-assemblyassembly {
// Compute and store the bytecode hash.mstore8(0x00, 0xff) // Write the prefix.mstore(0x35, hash)
mstore(0x01, shl(96, deployer))
mstore(0x15, salt)
predicted :=keccak256(0x00, 0x55)
// Restore the part of the free memory pointer that has been overwritten.mstore(0x35, 0)
}
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*//* READ LOGIC *//*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*//// @dev Returns all the `data` from the bytecode of the storage contract at `pointer`.functionread(address pointer) internalviewreturns (bytesmemory data) {
/// @solidity memory-safe-assemblyassembly {
let pointerCodesize :=extcodesize(pointer)
ifiszero(pointerCodesize) {
// Store the function selector of `InvalidPointer()`.mstore(0x00, 0x11052bb4)
// Revert with (offset, size).revert(0x1c, 0x04)
}
// Offset all indices by 1 to skip the STOP opcode.let size :=sub(pointerCodesize, DATA_OFFSET)
// Get the pointer to the free memory and allocate// enough 32-byte words for the data and the length of the data,// then copy the code to the allocated memory.// Masking with 0xffe0 will suffice, since contract size is less than 16 bits.
data :=mload(0x40)
mstore(0x40, add(data, and(add(size, 0x3f), 0xffe0)))
mstore(data, size)
mstore(add(add(data, 0x20), size), 0) // Zeroize the last slot.extcodecopy(pointer, add(data, 0x20), DATA_OFFSET, size)
}
}
/// @dev Returns the `data` from the bytecode of the storage contract at `pointer`,/// from the byte at `start`, to the end of the data stored.functionread(address pointer, uint256 start) internalviewreturns (bytesmemory data) {
/// @solidity memory-safe-assemblyassembly {
let pointerCodesize :=extcodesize(pointer)
ifiszero(pointerCodesize) {
// Store the function selector of `InvalidPointer()`.mstore(0x00, 0x11052bb4)
// Revert with (offset, size).revert(0x1c, 0x04)
}
// If `!(pointer.code.size > start)`, reverts.// This also handles the case where `start + DATA_OFFSET` overflows.ifiszero(gt(pointerCodesize, start)) {
// Store the function selector of `ReadOutOfBounds()`.mstore(0x00, 0x84eb0dd1)
// Revert with (offset, size).revert(0x1c, 0x04)
}
let size :=sub(pointerCodesize, add(start, DATA_OFFSET))
// Get the pointer to the free memory and allocate// enough 32-byte words for the data and the length of the data,// then copy the code to the allocated memory.// Masking with 0xffe0 will suffice, since contract size is less than 16 bits.
data :=mload(0x40)
mstore(0x40, add(data, and(add(size, 0x3f), 0xffe0)))
mstore(data, size)
mstore(add(add(data, 0x20), size), 0) // Zeroize the last slot.extcodecopy(pointer, add(data, 0x20), add(start, DATA_OFFSET), size)
}
}
/// @dev Returns the `data` from the bytecode of the storage contract at `pointer`,/// from the byte at `start`, to the byte at `end` (exclusive) of the data stored.functionread(address pointer, uint256 start, uint256 end)
internalviewreturns (bytesmemory data)
{
/// @solidity memory-safe-assemblyassembly {
let pointerCodesize :=extcodesize(pointer)
ifiszero(pointerCodesize) {
// Store the function selector of `InvalidPointer()`.mstore(0x00, 0x11052bb4)
// Revert with (offset, size).revert(0x1c, 0x04)
}
// If `!(pointer.code.size > end) || (start > end)`, revert.// This also handles the cases where// `end + DATA_OFFSET` or `start + DATA_OFFSET` overflows.ifiszero(
and(
gt(pointerCodesize, end), // Within bounds.iszero(gt(start, end)) // Valid range.
)
) {
// Store the function selector of `ReadOutOfBounds()`.mstore(0x00, 0x84eb0dd1)
// Revert with (offset, size).revert(0x1c, 0x04)
}
let size :=sub(end, start)
// Get the pointer to the free memory and allocate// enough 32-byte words for the data and the length of the data,// then copy the code to the allocated memory.// Masking with 0xffe0 will suffice, since contract size is less than 16 bits.
data :=mload(0x40)
mstore(0x40, add(data, and(add(size, 0x3f), 0xffe0)))
mstore(data, size)
mstore(add(add(data, 0x20), size), 0) // Zeroize the last slot.extcodecopy(pointer, add(data, 0x20), add(start, DATA_OFFSET), size)
}
}
}