generated from evan-gray/multichain-template
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
3fe58fe
commit 51b59d0
Showing
3 changed files
with
239 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
// SPDX-License-Identifier: Apache 2 | ||
pragma solidity >=0.8.8 <0.9.0; | ||
|
||
import "wormhole-solidity-sdk/libraries/BytesParsing.sol"; | ||
|
||
library AdapterInstructions { | ||
using BytesParsing for bytes; | ||
|
||
/// @notice Error thrown when there are too many instructions in the array. | ||
/// @dev Selector 0x3c46992e. | ||
error TooManyInstructions(); | ||
|
||
/// @notice Error thrown when the payload length exceeds the allowed maximum. | ||
/// @dev Selector 0xa3419691. | ||
/// @param size The size of the payload. | ||
error PayloadTooLong(uint256 size); | ||
|
||
/// @notice Error thrown when an Adapter instruction index | ||
/// is greater than the number of registered Adapters | ||
/// @dev We index from 0 so if providedIndex == numAdapters then we're out-of-bounds too | ||
/// @dev Selector 0x689f5016. | ||
/// @param providedIndex The index specified in the instruction | ||
/// @param numAdapters The number of registered Adapters | ||
error InvalidInstructionIndex(uint256 providedIndex, uint256 numAdapters); | ||
|
||
/// @dev Variable-length Adapter-specific instruction that can be passed by the integrator to the endpoint | ||
/// and by the endpoint to the adapter. | ||
/// The index field refers to the index of the adapter that this instruction should be passed to. | ||
/// The serialization format is: | ||
/// - index - 1 byte | ||
/// - payloadLength - 2 bytes | ||
/// - payload - `payloadLength` bytes | ||
struct AdapterInstruction { | ||
uint8 index; | ||
bytes payload; | ||
} | ||
|
||
/// @notice Encodes an adapter instruction. | ||
/// @param instruction The instruction to be encoded. | ||
/// @return encoded The encoded bytes, where the first byte is the index and the next two bytes are the instruction length. | ||
function encodeInstruction(AdapterInstruction memory instruction) public pure returns (bytes memory encoded) { | ||
if (instruction.payload.length > type(uint16).max) { | ||
revert PayloadTooLong(instruction.payload.length); | ||
} | ||
uint16 payloadLength = uint16(instruction.payload.length); | ||
encoded = abi.encodePacked(instruction.index, payloadLength, instruction.payload); | ||
} | ||
|
||
/// @notice Encodes an array of adapter instructions. | ||
/// @param instructions The array of instructions to be encoded. | ||
/// @return address The encoded bytes, where the first byte is the number of entries. | ||
function encodeInstructions(AdapterInstruction[] memory instructions) public pure returns (bytes memory) { | ||
if (instructions.length > type(uint8).max) { | ||
revert TooManyInstructions(); | ||
} | ||
uint256 instructionsLength = instructions.length; | ||
|
||
bytes memory encoded; | ||
for (uint256 i = 0; i < instructionsLength;) { | ||
bytes memory innerEncoded = encodeInstruction(instructions[i]); | ||
encoded = bytes.concat(encoded, innerEncoded); | ||
unchecked { | ||
++i; | ||
} | ||
} | ||
return abi.encodePacked(uint8(instructionsLength), encoded); | ||
} | ||
|
||
/// @notice Parses a byte array into an adapter instruction. | ||
/// @param encoded The encoded instruction. | ||
/// @return instruction The parsed instruction. | ||
function parseInstruction(bytes memory encoded) public pure returns (AdapterInstruction memory instruction) { | ||
uint256 offset = 0; | ||
(instruction, offset) = parseInstructionUnchecked(encoded, offset); | ||
encoded.checkLength(offset); | ||
} | ||
|
||
/// @notice Parses a byte array into an adapter instruction without checking for leftover bytes. | ||
/// @param encoded The buffer being parsed. | ||
/// @param offset The current offset into the encoded buffer. | ||
/// @return instruction The parsed instruction. | ||
/// @return nextOffset The next index into the array (used for further parsing). | ||
function parseInstructionUnchecked(bytes memory encoded, uint256 offset) | ||
public | ||
pure | ||
returns (AdapterInstruction memory instruction, uint256 nextOffset) | ||
{ | ||
(instruction.index, nextOffset) = encoded.asUint8Unchecked(offset); | ||
uint16 instructionLength; | ||
(instructionLength, nextOffset) = encoded.asUint16Unchecked(nextOffset); | ||
(instruction.payload, nextOffset) = encoded.sliceUnchecked(nextOffset, instructionLength); | ||
} | ||
|
||
/// @notice Parses a byte array into an array of adapter instructions. | ||
/// @param encoded The encoded instructions. | ||
/// @param numRegisteredAdapters The total number of registered adapters. | ||
/// @return instructions A sparse array of adapter instructions, where the index into the array is the adapter index. | ||
function parseInstructions(bytes memory encoded, uint256 numRegisteredAdapters) | ||
public | ||
pure | ||
returns (AdapterInstruction[] memory instructions) | ||
{ | ||
uint256 offset = 0; | ||
uint256 instructionsLength; | ||
(instructionsLength, offset) = encoded.asUint8Unchecked(offset); | ||
|
||
// We allocate an array with the length of the number of registered Adapters | ||
// This gives us the flexibility to not have to pass instructions for Adapters that | ||
// don't need them. | ||
instructions = new AdapterInstruction[](numRegisteredAdapters); | ||
|
||
for (uint256 i = 0; i < instructionsLength;) { | ||
AdapterInstruction memory instruction; | ||
(instruction, offset) = parseInstructionUnchecked(encoded, offset); | ||
|
||
uint8 instructionIndex = instruction.index; | ||
|
||
// Instruction index is out of bounds | ||
if (instructionIndex >= numRegisteredAdapters) { | ||
revert InvalidInstructionIndex(instructionIndex, numRegisteredAdapters); | ||
} | ||
|
||
instructions[instructionIndex] = instruction; | ||
unchecked { | ||
++i; | ||
} | ||
} | ||
|
||
encoded.checkLength(offset); | ||
} | ||
} |
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,107 @@ | ||
// SPDX-License-Identifier: Apache-2.0 | ||
pragma solidity ^0.8.13; | ||
|
||
import {Test, console} from "forge-std/Test.sol"; | ||
import "../src/libraries/AdapterInstructions.sol"; | ||
|
||
contract AdapterInstructionsTest is Test { | ||
function setUp() public {} | ||
|
||
function test_encodeInstruction() public { | ||
// Success case. | ||
bytes memory payload = "The payload"; | ||
bytes memory expected = abi.encodePacked(uint8(0), uint16(payload.length), payload); | ||
AdapterInstructions.AdapterInstruction memory inst = AdapterInstructions.AdapterInstruction(0, payload); | ||
bytes memory encoded = AdapterInstructions.encodeInstruction(inst); | ||
assertEq(keccak256(expected), keccak256(encoded)); | ||
|
||
// Payload too long. | ||
inst = AdapterInstructions.AdapterInstruction(0, new bytes(65537)); | ||
vm.expectRevert(abi.encodeWithSelector(AdapterInstructions.PayloadTooLong.selector, 65537)); | ||
AdapterInstructions.encodeInstruction(inst); | ||
} | ||
|
||
function test_encodeInstructions() public { | ||
// Success case. | ||
bytes memory expected = abi.encodePacked( | ||
uint8(3), | ||
uint8(0), | ||
uint16(29), | ||
"Instructions for adapter zero", | ||
uint8(3), | ||
uint16(30), | ||
"Instructions for adapter three", | ||
uint8(2), | ||
uint16(28), | ||
"Instructions for adapter two" | ||
); | ||
|
||
AdapterInstructions.AdapterInstruction[] memory insts = new AdapterInstructions.AdapterInstruction[](3); | ||
insts[0] = AdapterInstructions.AdapterInstruction(0, "Instructions for adapter zero"); | ||
insts[1] = AdapterInstructions.AdapterInstruction(3, "Instructions for adapter three"); | ||
insts[2] = AdapterInstructions.AdapterInstruction(2, "Instructions for adapter two"); | ||
bytes memory encoded = AdapterInstructions.encodeInstructions(insts); | ||
assertEq(keccak256(expected), keccak256(encoded)); | ||
|
||
// Too many instructions should revert. | ||
insts = new AdapterInstructions.AdapterInstruction[](257); | ||
for (uint256 idx = 0; idx < 257; ++idx) { | ||
insts[idx] = AdapterInstructions.AdapterInstruction(uint8(idx), "Some instruction"); | ||
} | ||
vm.expectRevert(abi.encodeWithSelector(AdapterInstructions.TooManyInstructions.selector)); | ||
encoded = AdapterInstructions.encodeInstructions(insts); | ||
} | ||
|
||
function test_parseInstruction() public pure { | ||
AdapterInstructions.AdapterInstruction memory expected = | ||
AdapterInstructions.AdapterInstruction(0, "The payload"); | ||
bytes memory encoded = AdapterInstructions.encodeInstruction(expected); | ||
AdapterInstructions.AdapterInstruction memory inst = AdapterInstructions.parseInstruction(encoded); | ||
assertEq(expected.index, inst.index); | ||
assertEq(keccak256(expected.payload), keccak256(inst.payload)); | ||
} | ||
|
||
// We need this to make the coverage tool happy, even though this function was called in the previous test. | ||
function test_parseInstructionUnchecked() public pure { | ||
AdapterInstructions.AdapterInstruction memory expected = | ||
AdapterInstructions.AdapterInstruction(0, "The payload"); | ||
bytes memory encoded = AdapterInstructions.encodeInstruction(expected); | ||
(AdapterInstructions.AdapterInstruction memory inst, uint256 nextOffset) = | ||
AdapterInstructions.parseInstructionUnchecked(encoded, 0); | ||
assertEq(expected.index, inst.index); | ||
assertEq(keccak256(expected.payload), keccak256(inst.payload)); | ||
assertEq(encoded.length, nextOffset); | ||
} | ||
|
||
function test_parseInstructions() public { | ||
// Success case. | ||
bytes memory expectedInst0 = "Instructions for adapter zero"; | ||
bytes memory expectedInst2 = "Instructions for adapter two"; | ||
bytes memory expectedInst3 = "Instructions for adapter three"; | ||
AdapterInstructions.AdapterInstruction[] memory expected = new AdapterInstructions.AdapterInstruction[](3); | ||
expected[0] = AdapterInstructions.AdapterInstruction(0, expectedInst0); | ||
expected[1] = AdapterInstructions.AdapterInstruction(3, expectedInst3); | ||
expected[2] = AdapterInstructions.AdapterInstruction(2, expectedInst2); | ||
bytes memory encoded = AdapterInstructions.encodeInstructions(expected); | ||
|
||
AdapterInstructions.AdapterInstruction[] memory insts = AdapterInstructions.parseInstructions(encoded, 4); | ||
assertEq(4, insts.length); | ||
|
||
assertEq(0, insts[0].index); | ||
assertEq(keccak256(expectedInst0), keccak256(insts[0].payload)); | ||
|
||
// Entry one should be empty. | ||
assertEq(0, insts[1].index); | ||
assertEq(0, insts[1].payload.length); | ||
|
||
assertEq(2, insts[2].index); | ||
assertEq(keccak256(expectedInst2), keccak256(insts[2].payload)); | ||
|
||
assertEq(3, insts[3].index); | ||
assertEq(keccak256(expectedInst3), keccak256(insts[3].payload)); | ||
|
||
// Index out of range should revert. | ||
vm.expectRevert(abi.encodeWithSelector(AdapterInstructions.InvalidInstructionIndex.selector, 3, 3)); | ||
AdapterInstructions.parseInstructions(encoded, 3); | ||
} | ||
} |
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