Skip to content
Open
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
54 changes: 54 additions & 0 deletions src/interface/IInterpreterCallerV4.sol
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,60 @@ struct EvaluableV4 {
bytes bytecode;
}

/// @dev EIP-712 type of the data a `SignedContextV2` signature is over: the signer
/// and the context words. The `signature` field is not a member.
string constant SIGNED_CONTEXT_V2_TYPE = "SignedContextV2(address signer,bytes32[] context)";

/// @dev `keccak256` of `SIGNED_CONTEXT_V2_TYPE`, written as a literal so the hash
/// is computed at compile time.
bytes32 constant SIGNED_CONTEXT_V2_TYPEHASH = keccak256("SignedContextV2(address signer,bytes32[] context)");

/// Typed embodiment of some context data with associated signer and signature,
/// signed as EIP-712 typed data. The signature MUST be over the digest
/// `keccak256(abi.encodePacked(hex"1901", domainSeparator, hashStruct))` where
/// `hashStruct` is `LibContext.hashStruct`, i.e.
/// `keccak256(abi.encodePacked(SIGNED_CONTEXT_V2_TYPEHASH, signer, keccak256(abi.encodePacked(context))))`,
/// and `domainSeparator` is the EIP-712 domain separator of the calling
/// contract, passed by it to `LibContext.buildV2`. The calling contract chooses
/// its domain (which fields it has and their values) and MAY publish it per
/// ERC-5267; the same signed data under a different domain separator does not
/// verify.
///
/// The domain separates this calling contract's signed contexts from other
/// domains. It is not replay protection: the calling contract (likely with the
/// help of `LibContext`) is responsible for ensuring the authenticity of the
/// signature, but not authorizing _who_ can sign. IN ADDITION to authorisation
/// of the signer to known-good entities the expression is also responsible for:
///
/// - Enforcing the context is the expected data (e.g. a word identifying what
/// the signed context is for)
/// - Tracking and enforcing nonces if signed contexts are only usable one time
/// - Tracking and enforcing uniqueness of signed data if relevant
/// - Checking and enforcing expiry times if present and relevant in the context
/// - Many other potential constraints that expressions may want to enforce
///
/// EIP-1271 smart contract signatures are supported in addition to EOA
/// signatures via. the Open Zeppelin `SignatureChecker` library, which is
/// wrapped by `LibContext.buildV2`. As smart contract signatures are checked
/// onchain they CAN BE REVOKED AT ANY MOMENT as the smart contract can simply
/// return `false` when it previously returned `true`.
///
/// A `SignedContextV1` signature (EIP-191 `personal_sign` over the context
/// hash) is not a valid `SignedContextV2` signature, and vice versa.
///
/// @param signer The account that produced the signature. Part of the signed
/// data, so a signature verifies for exactly one signer, for smart contract
/// signers too.
/// @param context The signed data in a format that can be merged into a
/// 2-dimensional context matrix as-is.
/// @param signature The EIP-712 signature over `signer` and `context` under
/// the calling contract's domain. Not part of the signed data.
struct SignedContextV2 {
address signer;
bytes32[] context;
bytes signature;
}

/// @title IInterpreterCallerV4
/// @notice A contract that calls an `IInterpreterV4` via. `eval4`. There are
/// near zero requirements on a caller other than:
Expand Down
102 changes: 101 additions & 1 deletion src/lib/caller/LibContext.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import {
SignedContextV1,
SIGNED_CONTEXT_SIGNER_OFFSET,
SIGNED_CONTEXT_SIGNATURE_OFFSET,
SIGNED_CONTEXT_CONTEXT_OFFSET
SIGNED_CONTEXT_CONTEXT_OFFSET,
SignedContextV2,
SIGNED_CONTEXT_V2_TYPEHASH
} from "../../interface/IInterpreterCallerV4.sol";

/// Thrown when the ith signature from a list of signed contexts is invalid.
Expand Down Expand Up @@ -221,4 +223,102 @@ library LibContext {
return context;
}
}

/// EIP-712 `hashStruct` of a `SignedContextV2`:
/// `keccak256(abi.encodePacked(SIGNED_CONTEXT_V2_TYPEHASH, signer, keccak256(abi.encodePacked(context))))`.
/// The context is hashed as its packed 32-byte words with no length
/// prefix, which is the EIP-712 encoding of a `bytes32[]` member. The
/// `signature` field is not part of the hash. The three words are written
/// to the memory past the free memory pointer, which is not moved, so
/// nothing is allocated.
/// @param signedContext The signed context to hash.
/// @return hashed The EIP-712 struct hash of `signedContext`.
function hashStruct(SignedContextV2 memory signedContext) internal pure returns (bytes32 hashed) {
bytes32 typeHash = SIGNED_CONTEXT_V2_TYPEHASH;
address signer = signedContext.signer;
bytes32[] memory context = signedContext.context;
assembly ("memory-safe") {
let contextHash := keccak256(add(context, 0x20), mul(mload(context), 0x20))
let ptr := mload(0x40)
mstore(ptr, typeHash)
mstore(add(ptr, 0x20), signer)
mstore(add(ptr, 0x40), contextHash)
hashed := keccak256(ptr, 0x60)
}
}

/// Builds a standard 2-dimensional context array from base, calling and
/// signed contexts, as `build` does, with each `SignedContextV2` verified
/// as EIP-712 typed data under `domainSeparator`. The returned matrix has
/// the same layout as `build`: column 0 is `LibContext.base()`, then the
/// `baseContext` columns, then (only if there are signed contexts) a
/// column of the signers in order and one column per signed context.
///
/// @param baseContext Anything the calling contract can provide which MAY
/// include input from the `msg.sender` of the calling contract. The default
/// base context from `LibContext.base()` DOES NOT need to be provided by the
/// caller, this matrix MAY be empty and will be simply merged into the final
/// context. The base context matrix MUST contain a consistent number of
/// columns from the calling contract so that the expression can always
/// predict how many unsigned columns there will be when it runs.
/// @param signedContexts Signed contexts are provided by the `msg.sender`
/// but signed by a third party. Each signature is verified for its
/// `signer` against
/// `MessageHashUtils.toTypedDataHash(domainSeparator, hashStruct(signedContext))`
/// through `SignatureChecker`, so EOA and ERC-1271 signers are both
/// supported. REVERTS with `InvalidSignature(i)` at the first `i` that
/// does not verify. The expression (author) defines _who_ may sign, and
/// the binding of the signed words to a particular use (nonces, expiries,
/// what the words are for) is expressed in the context words and checked
/// by the expression, as with `build`. The `msg.sender` can provide an
/// arbitrary number of signed contexts so expressions DO NOT know exactly
/// how many columns there are.
/// @param domainSeparator The EIP-712 domain separator of the calling
/// contract. This library computes no domain and fixes no domain fields.
/// The same signed data under a different domain separator does not
/// verify.
/// @return The fully assembled context matrix, laid out as `build`.
function buildV2(bytes32[][] memory baseContext, SignedContextV2[] memory signedContexts, bytes32 domainSeparator)
internal
view
returns (bytes32[][] memory)
{
unchecked {
bytes32[] memory signers = new bytes32[](signedContexts.length);

// - LibContext.base() + whatever we are provided.
// - signed contexts + signers if they exist else nothing.
uint256 contextLength = 1 + baseContext.length + (signedContexts.length > 0 ? signedContexts.length + 1 : 0);

bytes32[][] memory context = new bytes32[][](contextLength);
uint256 offset = 0;
context[offset] = LibContext.base();

for (uint256 i = 0; i < baseContext.length; i++) {
offset++;
context[offset] = baseContext[i];
}

if (signedContexts.length > 0) {
offset++;
context[offset] = signers;

for (uint256 i = 0; i < signedContexts.length; i++) {
if (!SignatureChecker.isValidSignatureNow(
signedContexts[i].signer,
MessageHashUtils.toTypedDataHash(domainSeparator, hashStruct(signedContexts[i])),
signedContexts[i].signature
)) {
revert InvalidSignature(i);
}

signers[i] = bytes32(uint256(uint160(signedContexts[i].signer)));
offset++;
context[offset] = signedContexts[i].context;
}
}

return context;
}
}
}
105 changes: 105 additions & 0 deletions test/lib/caller/LibSignedContextV2TypedData.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// SPDX-License-Identifier: LicenseRef-DCL-1.0
// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd
pragma solidity =0.8.25;

import {Vm} from "forge-std-1.16.2/src/Vm.sol";
import {SignedContextV2, SIGNED_CONTEXT_V2_TYPE} from "src/interface/IInterpreterCallerV4.sol";

/// The EIP-712 message of a `SignedContextV2`: its typed members, without the
/// signature. ABI-encoded, this is what the struct hash cheatcode decodes.
struct SignedContextV2TypedData {
address signer;
bytes32[] context;
}

/// An EIP-712 domain with the four standard fields.
struct EIP712Domain {
string name;
string version;
uint256 chainId;
address verifyingContract;
}

/// EIP-712 oracle values for `SignedContextV2`, all derived by forge-std
/// cheatcodes from the type strings and EIP-712 JSON. Nothing here hashes
/// anything itself.
library LibSignedContextV2TypedData {
Vm constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code")))));

string constant EIP712_DOMAIN_TYPE =
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)";

/// ABI encoding of the typed members of `signedContext`.
function typedData(SignedContextV2 memory signedContext) internal pure returns (bytes memory) {
return abi.encode(SignedContextV2TypedData({signer: signedContext.signer, context: signedContext.context}));
}

/// `hashStruct(signedContext)` from the cheatcode.
function hashStruct(SignedContextV2 memory signedContext) internal pure returns (bytes32) {
return vm.eip712HashStruct(SIGNED_CONTEXT_V2_TYPE, typedData(signedContext));
}

/// `hashStruct(domain)` from the cheatcode.
function domainSeparator(EIP712Domain memory domain) internal pure returns (bytes32) {
return vm.eip712HashStruct(EIP712_DOMAIN_TYPE, abi.encode(domain));
}

/// JSON array of the context words as 32-byte hex strings.
function contextJson(bytes32[] memory context) internal pure returns (string memory words) {
words = "[";
for (uint256 i = 0; i < context.length; i++) {
words = string.concat(words, i == 0 ? "" : ",", "\"", vm.toString(context[i]), "\"");
}
words = string.concat(words, "]");
}

/// EIP-712 JSON (`types`, `primaryType`, `domain`, `message`) for `signer`
/// and `context` under `domain`, as `eth_signTypedData_v4` takes it.
function json(EIP712Domain memory domain, address signer, bytes32[] memory context)
internal
pure
returns (string memory)
{
return string.concat(
"{\"types\":{\"EIP712Domain\":[{\"name\":\"name\",\"type\":\"string\"},"
"{\"name\":\"version\",\"type\":\"string\"},{\"name\":\"chainId\",\"type\":\"uint256\"},"
"{\"name\":\"verifyingContract\",\"type\":\"address\"}],"
"\"SignedContextV2\":[{\"name\":\"signer\",\"type\":\"address\"},{\"name\":\"context\",\"type\":\"bytes32[]\"}]},"
"\"primaryType\":\"SignedContextV2\",",
"\"domain\":{\"name\":\"",
domain.name,
"\",\"version\":\"",
domain.version,
"\",\"chainId\":",
vm.toString(domain.chainId),
",\"verifyingContract\":\"",
vm.toString(domain.verifyingContract),
"\"},",
"\"message\":{\"signer\":\"",
vm.toString(signer),
"\",\"context\":",
contextJson(context),
"}}"
);
}

/// The digest a wallet signs for `signer` and `context` under `domain`,
/// from the cheatcode over the JSON.
function digest(EIP712Domain memory domain, address signer, bytes32[] memory context)
internal
pure
returns (bytes32)
{
return vm.eip712HashTypedData(json(domain, signer, context));
}

/// An EOA signature by `privateKey` over `digest(domain, vm.addr(privateKey), context)`.
function sign(uint256 privateKey, EIP712Domain memory domain, bytes32[] memory context)
internal
pure
returns (bytes memory)
{
(uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest(domain, vm.addr(privateKey), context));
return abi.encodePacked(r, s, v);
}
}
Loading
Loading