-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support to emit off-chain logs returned in the Suave Transaction (#…
…56) * Add tests * Looks good * More cleaning * Rename modifier * Commit Suapp contract * Update forge * Show suave-geth version on ci * Do not use assertNotEq * Forgot something
- Loading branch information
Showing
5 changed files
with
279 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Submodule forge-std
updated
5 files
+2 −0 | src/StdChains.sol | |
+201 −106 | src/StdStorage.sol | |
+2 −0 | test/StdChains.t.sol | |
+10 −0 | test/StdCheats.t.sol | |
+159 −11 | test/StdStorage.t.sol |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
// SPDX-License-Identifier: Unlicense | ||
pragma solidity ^0.8.13; | ||
|
||
import "./suavelib/Suave.sol"; | ||
|
||
library Logs { | ||
struct Log { | ||
address addr; | ||
bytes32[] topics; | ||
bytes data; | ||
} | ||
|
||
bytes constant MAGIC_SEQUENCE = hex"543543"; | ||
|
||
function findStartIndex(bytes memory data) internal pure returns (uint256) { | ||
for (uint256 i = 0; i < data.length; i++) { | ||
if (data[i] == MAGIC_SEQUENCE[0]) { | ||
// Check if current byte matches the first byte of MAGIC_SEQUENCE | ||
bool isMatch = true; | ||
for (uint256 j = 1; j < MAGIC_SEQUENCE.length; j++) { | ||
// Check subsequent bytes | ||
if (i + j >= data.length || data[i + j] != MAGIC_SEQUENCE[j]) { | ||
isMatch = false; | ||
break; | ||
} | ||
} | ||
if (isMatch) { | ||
return i + MAGIC_SEQUENCE.length; // Return the index after the magic sequence | ||
} | ||
} | ||
} | ||
return data.length; // Not found | ||
} | ||
|
||
function decodeLogs(bytes memory inputData) internal { | ||
uint256 magicSequenceIndex = findStartIndex(inputData); | ||
if (magicSequenceIndex == inputData.length) { | ||
return; // Magic sequence not found, skip logs | ||
} | ||
|
||
// Calculate the length of the data to decode | ||
uint256 dataLength = inputData.length - magicSequenceIndex; | ||
|
||
// Initialize memory for the data to decode | ||
bytes memory dataToDecode = new bytes(dataLength); | ||
|
||
// Copy the data to decode into the memory array | ||
for (uint256 i = 0; i < dataLength; i++) { | ||
dataToDecode[i] = inputData[magicSequenceIndex + i]; | ||
} | ||
|
||
(Log[] memory logs) = abi.decode(dataToDecode, (Log[])); | ||
for (uint256 i = 0; i < logs.length; i++) { | ||
emitLog(logs[i]); | ||
} | ||
} | ||
|
||
function emitLog(Log memory log) internal { | ||
bytes memory logData = log.data; | ||
uint256 dataLength = logData.length; | ||
|
||
if (log.topics.length == 0) { | ||
assembly { | ||
log0(add(logData, 32), dataLength) | ||
} | ||
} else if (log.topics.length == 1) { | ||
bytes32 topic0 = log.topics[0]; | ||
|
||
assembly { | ||
log1(add(logData, 32), dataLength, topic0) | ||
} | ||
} else if (log.topics.length == 2) { | ||
bytes32 topic0 = log.topics[0]; | ||
bytes32 topic1 = log.topics[1]; | ||
|
||
assembly { | ||
log2(add(logData, 32), dataLength, topic0, topic1) | ||
} | ||
} else if (log.topics.length == 3) { | ||
bytes32 topic0 = log.topics[0]; | ||
bytes32 topic1 = log.topics[1]; | ||
bytes32 topic2 = log.topics[2]; | ||
|
||
assembly { | ||
log3(add(logData, 32), dataLength, topic0, topic1, topic2) | ||
} | ||
} else if (log.topics.length == 4) { | ||
bytes32 topic0 = log.topics[0]; | ||
bytes32 topic1 = log.topics[1]; | ||
bytes32 topic2 = log.topics[2]; | ||
bytes32 topic3 = log.topics[3]; | ||
|
||
assembly { | ||
log4(add(logData, 32), dataLength, topic0, topic1, topic2, topic3) | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
// SPDX-License-Identifier: UNLICENSED | ||
pragma solidity ^0.8.8; | ||
|
||
import "./Logs.sol"; | ||
|
||
contract Suapp { | ||
modifier emitOffchainLogs() { | ||
Logs.decodeLogs(msg.data); | ||
_; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
// SPDX-License-Identifier: Unlicense | ||
pragma solidity ^0.8.13; | ||
|
||
import "forge-std/Test.sol"; | ||
import "forge-std/console.sol"; | ||
import "forge-std/Vm.sol"; | ||
import "src/Logs.sol"; | ||
import "src/Suapp.sol"; | ||
|
||
contract TestLogs is Test { | ||
// generated with suave-geth TestE2E_EmitLogs test | ||
bytes constant encodedResultTestcase = | ||
hex"0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000420000000000000000000000000030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000001dc013ea64717e828d19b2a2ee201871627a4a65b8e96984868b9391a327be18a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000023ab4eb7c630180e1144d0a956c71b3cb538ea0b9566b6136fe7a87cb222249d20000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000039e4d9541d8ffa37d1346e0a2ee0dd6cb444641c8458993777d3d06e9da3bd66600000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000004b1b8bb6d3c04a82bd66f1d7dbecf2754edafb96d47af76845f87c5245afaec2d00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003"; | ||
|
||
// getEmittedLog returns the emitted logs in Forge as a Log struct. | ||
function getEmittedLogs() private returns (Logs.Log[] memory) { | ||
VmSafe.Log[] memory logs = vm.getRecordedLogs(); | ||
|
||
Logs.Log[] memory logs11 = new Logs.Log[](logs.length); | ||
for (uint256 i = 0; i < logs.length; i++) { | ||
Logs.Log memory log; | ||
log.addr = logs[i].emitter; | ||
log.topics = logs[i].topics; | ||
log.data = logs[i].data; | ||
|
||
logs11[i] = log; | ||
} | ||
|
||
return logs11; | ||
} | ||
|
||
// equalLogs checks if two Log structs are equal. | ||
function equalLogs(Logs.Log memory a, Logs.Log memory b) internal { | ||
assertEq(a.addr, b.addr); | ||
assertEq(a.topics.length, b.topics.length); | ||
for (uint256 i = 0; i < a.topics.length; i++) { | ||
assertEq(a.topics[i], b.topics[i]); | ||
} | ||
assertEq(a.data, b.data); | ||
} | ||
|
||
event EventAnonymous() anonymous; | ||
event EventTopic1(); | ||
event EventTopic2(uint256 indexed num1, uint256 numNoIndex); | ||
event EventTopic3(uint256 indexed num1, uint256 indexed num2, uint256 numNoIndex); | ||
event EventTopic4(uint256 indexed num1, uint256 indexed num2, uint256 indexed num3, uint256 numNoIndex); | ||
event EventWithMultipleArgs(uint256 indexed num1, uint256 num2, uint256 num3); | ||
|
||
// testEmitLog tests that the logs emitted with 'emitLog' have the same | ||
// values as the logs emitted with 'emit '. | ||
function testExecResult_EmitLog() public { | ||
vm.recordLogs(); | ||
|
||
emit EventAnonymous(); | ||
emit EventTopic1(); | ||
emit EventTopic2(1, 1); | ||
emit EventTopic3(1, 2, 2); | ||
emit EventTopic4(1, 2, 3, 4); | ||
emit EventWithMultipleArgs(1, 2, 3); | ||
|
||
// get all the emitted logs and send them again with | ||
// the low-level emitLog function | ||
Logs.Log[] memory logs = getEmittedLogs(); | ||
|
||
for (uint256 i = 0; i < logs.length; i++) { | ||
Logs.emitLog(logs[i]); | ||
} | ||
|
||
// get the emitted logs from 'emitLog' and compare them with the | ||
// original emitted logs | ||
Logs.Log[] memory logs1 = getEmittedLogs(); | ||
|
||
for (uint256 i = 0; i < logs.length; i++) { | ||
equalLogs(logs[i], logs1[i]); | ||
} | ||
} | ||
|
||
function testExecResult_DecodeLogsFull() public { | ||
// emit and record some logs to build the Result struct | ||
vm.recordLogs(); | ||
|
||
emit EventAnonymous(); | ||
emit EventTopic1(); | ||
emit EventTopic2(1, 1); | ||
|
||
Logs.Log[] memory logs = getEmittedLogs(); | ||
bytes memory encodedResult = abi.encode(logs); | ||
|
||
// create some dummy prefix + MAGIC + encodedResult | ||
bytes memory inputData = abi.encodePacked(hex"00112233445566", Logs.MAGIC_SEQUENCE, encodedResult); | ||
|
||
Logs.decodeLogs(inputData); | ||
|
||
// validate that the logs were emitted again | ||
Logs.Log[] memory logs1 = getEmittedLogs(); | ||
for (uint256 i = 0; i < logs.length; i++) { | ||
equalLogs(logs[i], logs1[i]); | ||
} | ||
} | ||
|
||
function testExecResult_LogsTestcases() public { | ||
// validate that we can parse the result testcase generated by suave-geth | ||
(Logs.Log[] memory logs) = abi.decode(encodedResultTestcase, (Logs.Log[])); | ||
|
||
assertEq(logs.length, 5); | ||
assertEq(logs[0].topics.length, 0); | ||
assertEq(logs[1].topics.length, 1); | ||
assertEq(logs[2].topics.length, 2); | ||
assertEq(logs[3].topics.length, 3); | ||
assertEq(logs[4].topics.length, 4); | ||
|
||
// 0 and 1 do not have data and 2.. have data | ||
assertEq(logs[0].data.length, 0); | ||
assertEq(logs[1].data.length, 0); | ||
for (uint256 i = 2; i < logs.length; i++) { | ||
if (logs[i].data.length == 0) { | ||
revert("data is empty"); | ||
} | ||
} | ||
|
||
// all of them have the same address (0x030) | ||
for (uint256 i = 0; i < logs.length; i++) { | ||
assertEq(logs[i].addr, 0x0300000000000000000000000000000000000000); | ||
} | ||
|
||
// validate that 'decodeLogs' works too | ||
bytes memory inputData = abi.encodePacked(hex"00112233445566", Logs.MAGIC_SEQUENCE, encodedResultTestcase); | ||
|
||
vm.recordLogs(); | ||
Logs.decodeLogs(inputData); | ||
|
||
Logs.Log[] memory foundLogs = getEmittedLogs(); | ||
assertEq(foundLogs.length, logs.length); | ||
} | ||
|
||
// testFindStartIndex tests that we can detect when the logs suffix starts | ||
function testExecResult_FindStartIndex() public { | ||
bytes memory noMagic = hex"00112233445566"; | ||
uint256 index = Logs.findStartIndex(noMagic); | ||
assertEq(index, noMagic.length); | ||
|
||
bytes memory withMagic = abi.encodePacked(noMagic, Logs.MAGIC_SEQUENCE, hex"778899aabbcc"); | ||
|
||
index = Logs.findStartIndex(withMagic); | ||
assertEq(index, noMagic.length + Logs.MAGIC_SEQUENCE.length); | ||
} | ||
|
||
// testExecResult_Modifier tests that the Suapp modifier works | ||
function testExecResult_Modifier() public { | ||
SuappExample suapp = new SuappExample(); | ||
|
||
// call suapp.testEmit with empty input + the magic sequence + logs | ||
bytes memory callSig = abi.encodeWithSelector(SuappExample.testEmit.selector); | ||
bytes memory inputData = abi.encodePacked(callSig, Logs.MAGIC_SEQUENCE, encodedResultTestcase); | ||
|
||
// call the function | ||
vm.recordLogs(); | ||
(bool success,) = address(suapp).call(inputData); | ||
assertEq(success, true); | ||
|
||
Logs.Log[] memory foundLogs = getEmittedLogs(); | ||
assertEq(foundLogs.length, 5); | ||
} | ||
} | ||
|
||
contract SuappExample is Suapp { | ||
function testEmit() public emitOffchainLogs {} | ||
} |