Skip to content

Commit

Permalink
evm: Add AdapterInstructions
Browse files Browse the repository at this point in the history
  • Loading branch information
bruce-riley committed Dec 9, 2024
1 parent 3fe58fe commit 51b59d0
Show file tree
Hide file tree
Showing 3 changed files with 239 additions and 1 deletion.
131 changes: 131 additions & 0 deletions evm/src/libraries/AdapterInstructions.sol
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);
}
}
107 changes: 107 additions & 0 deletions evm/test/AdapterInstructions.t.sol
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);
}
}
2 changes: 1 addition & 1 deletion evm/test/AdapterRegistry.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ contract AdapterRegistryTest is Test {
assertEq(adapterRegistry.getEnabledRecvAdaptersBitmapForChain(integrator1, zeroChain), 0);
}

// This is a redudant test, as the previous tests already cover this
// This is a redundant test, as the previous tests already cover this
function test5() public view {
// Send side
assertEq(adapterRegistry.getRegisteredAdaptersStorage(integrator1).length, 0);
Expand Down

0 comments on commit 51b59d0

Please sign in to comment.