Skip to content

Latest commit

 

History

History
100 lines (63 loc) · 3.3 KB

File metadata and controls

100 lines (63 loc) · 3.3 KB

Mammoth Mocha Koala

Medium

Missing Domain-Caller Validation

Summary

https://github.com/sherlock-audit/2025-01-perennial-v2-4-update/blob/main/perennial-v2/packages/core/contracts/Verifier.sol#L42C5-L52C1

The Verifier contract fails to enforce that a signed message’s domain field (if set) matches the address of the contract calling the verification function (msg.sender). This allows signatures intended for one context (e.g., a specific market) to be replayed in unintended contexts (e.g., a different market), violating domain isolation and enabling signature reuse across domains

Root Cause

https://github.com/sherlock-audit/2025-01-perennial-v2-4-update/blob/main/perennial-v2/packages/core/contracts/Verifier.sol#L42C5-L52C1

function verifyFill(Fill calldata fill, bytes calldata signature)
    external
    validateAndCancel(fill.common, signature)
{
    if (!SignatureChecker.isValidSignatureNow(
        fill.common.signer,
        _hashTypedDataV4(FillLib.hash(fill)),
        signature
    )) revert VerifierInvalidSignerError();
}

EIP-712 signatures include a domain separator to cryptographically bind a signature to a specific context (e.g., a specific contract or "domain"). This prevents replaying signatures across different contexts.

In the Verifier contract, messages like Fill, Intent, or Take include a common.domain field. If set, this domain should restrict the message’s validity to only the specified contract (e.g., a market address).

The Verifier contract does not validate that msg.sender matches common.domain during signature verification. For example:

function verifyFill(Fill calldata fill, bytes calldata signature) external validateAndCancel(fill.common, signature) { // No check that msg.sender == fill.common.domain if (!SignatureChecker.isValidSignatureNow(...)) revert(...); } This allows a malicious actor to submit a message with domain = Market A to Market B, bypassing the intended domain isolation.

Internal Pre-conditions

No response

External Pre-conditions

No response

Attack Path

Valid Signature for Market A:

A user signs a Fill message with domain = Market A.

The message is intended to execute a trade only in Market A.

Replay Attack on Market B:

An attacker calls Verifier.verifyFill(fillMessage, signature) from Market B (not Market A).

The Verifier approves the signature because it does not check if msg.sender == fill.common.domain.

Unauthorized Execution:

Market B processes the fill intended for Market A, leading to unintended trades, liquidity manipulation, or financial loss.

Impact

Attackers can reuse signatures in unintended contexts (e.g., different markets). Valid signatures meant for one domain can execute actions in another domain, violating trust boundaries.

PoC

No response

Mitigation

Add a check to enforce that msg.sender matches the common.domain (if the domain is set):

modifier validateDomain(Common calldata common) { if (common.domain != address(0) && common.domain != msg.sender) { revert VerifierInvalidDomainError(); } _; }

// Apply modifier to verification functions: function verifyFill(Fill calldata fill, bytes calldata signature) external validateDomain(fill.common) // validateAndCancel(fill.common, signature) { // ... }