Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TD-1392] Immutable signed zone v2 documentation #203

Merged
merged 8 commits into from
Apr 15, 2024
Merged
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
15 changes: 15 additions & 0 deletions audits/trading/202404-threat-model-immutable-signed-zone-v2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Immutable Signed Zone (v2) Threat Model

## Introduction

This document is a threat model for the [Immutable Signed Zone (v2)](../../contracts/trading/seaport/zones/immutable-signed-zone/v2/README.md) contract built by Immutable.

## Architecture

## Attack Surfaces

## Perceived Attackers

## Attack Mitigation

## Conclusion
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,18 @@
// SPDX-License-Identifier: Apache-2

// solhint-disable-next-line compiler-version
pragma solidity ^0.8.17;
pragma solidity ^0.8.20;

import {ZoneParameters, Schema, ReceivedItem} from "seaport-types/src/lib/ConsiderationStructs.sol";
import {ZoneInterface} from "seaport/contracts/interfaces/ZoneInterface.sol";
import {ZoneParameters, Schema, ReceivedItem} from "seaport-types/src/lib/ConsiderationStructs.sol";
import {AccessControlEnumerable} from "openzeppelin-contracts-5.0.2/access/extensions/AccessControlEnumerable.sol";
import {ECDSA} from "openzeppelin-contracts-5.0.2/utils/cryptography/ECDSA.sol";
import {MessageHashUtils} from "openzeppelin-contracts-5.0.2/utils/cryptography/MessageHashUtils.sol";
import {ERC165} from "openzeppelin-contracts-5.0.2/utils/introspection/ERC165.sol";
import {Math} from "openzeppelin-contracts-5.0.2/utils/math/Math.sol";
import {SIP5Interface} from "./interfaces/SIP5Interface.sol";
import {SIP6Interface} from "./interfaces/SIP6Interface.sol";
import {SIP7Interface} from "./interfaces/SIP7Interface.sol";
import {AccessControlEnumerable} from "@openzeppelin/contracts/access/AccessControlEnumerable.sol";
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
import {Math} from "openzeppelin-contracts-5.0.2/utils/math/Math.sol";

/**
* @title ImmutableSignedZoneV2
Expand All @@ -29,40 +30,44 @@ contract ImmutableSignedZoneV2 is
SIP7Interface,
AccessControlEnumerable
{
/// @dev The EIP-712 digest parameters.
bytes32 internal immutable _VERSION_HASH = keccak256(bytes("2.0"));
bytes32 internal immutable _EIP_712_DOMAIN_TYPEHASH = keccak256(
/// @dev The EIP-712 domain type hash.
bytes32 private constant _EIP_712_DOMAIN_TYPEHASH = keccak256(
abi.encodePacked(
"EIP712Domain(", "string name,", "string version,", "uint256 chainId,", "address verifyingContract", ")"
)
);

bytes32 internal immutable _SIGNED_ORDER_TYPEHASH = keccak256(
/// @dev The EIP-712 domain version value.
bytes32 private constant _VERSION_HASH = keccak256(bytes("2.0"));

/// @dev The EIP-712 signed order type hash.
bytes32 private constant _SIGNED_ORDER_TYPEHASH = keccak256(
abi.encodePacked(
"SignedOrder(", "address fulfiller,", "uint64 expiration,", "bytes32 orderHash,", "bytes context", ")"
)
);

uint256 internal immutable _CHAIN_ID = block.chainid;
bytes32 internal immutable _DOMAIN_SEPARATOR;
uint8 internal immutable _ACCEPTED_SIP6_VERSION = 0;
/// @dev The chain ID on which the contract was deployed.
uint256 private immutable _CHAIN_ID = block.chainid;

/// @dev The domain separator used for signing.
bytes32 private immutable _DOMAIN_SEPARATOR;

/// @dev The accepted SIP-6 version.
uint8 private constant _ACCEPTED_SIP6_VERSION = 0;

/// @dev The name for this zone returned in getSeaportMetadata().
// solhint-disable-next-line var-name-mixedcase
string private _ZONE_NAME;

// slither-disable-start immutable-states
// solhint-disable-next-line var-name-mixedcase
bytes32 internal _NAME_HASH;
// slither-disable-end immutable-states
bytes32 private immutable _NAME_HASH;

/// @dev The allowed signers.
// solhint-disable-next-line named-parameters-mapping
mapping(address => SignerInfo) private _signers;

/// @dev The API endpoint where orders for this zone can be signed.
/// Request and response payloads are defined in SIP-7.
string private _sip7APIEndpoint;
string private _apiEndpoint;

/// @dev The documentationURI.
string private _documentationURI;
Expand All @@ -85,7 +90,9 @@ contract ImmutableSignedZoneV2 is
_NAME_HASH = keccak256(bytes(zoneName));

// Set the API endpoint.
_sip7APIEndpoint = apiEndpoint;
_apiEndpoint = apiEndpoint;

// Set the documentation URI.
_documentationURI = documentationURI;

// Derive and set the domain separator.
Expand Down Expand Up @@ -152,8 +159,20 @@ contract ImmutableSignedZoneV2 is
* @param newApiEndpoint The new API endpoint.
*/
function updateAPIEndpoint(string calldata newApiEndpoint) external override onlyRole(DEFAULT_ADMIN_ROLE) {
// Update to the new API endpoint.
_sip7APIEndpoint = newApiEndpoint;
_apiEndpoint = newApiEndpoint;
}

/**
* @notice Update the documentation URI returned by this zone.
*
* @param newDocumentationURI The new documentation URI.
*/
function updateDocumentationURI(string calldata newDocumentationURI)
external
override
onlyRole(DEFAULT_ADMIN_ROLE)
{
_documentationURI = newDocumentationURI;
}

/**
Expand All @@ -175,7 +194,7 @@ contract ImmutableSignedZoneV2 is
schemas = new Schema[](1);
schemas[0].id = 7;
schemas[0].metadata =
abi.encode(_domainSeparator(), _sip7APIEndpoint, _getSupportedSubstandards(), _documentationURI);
abi.encode(_domainSeparator(), _apiEndpoint, _getSupportedSubstandards(), _documentationURI);
}

/**
Expand All @@ -198,36 +217,21 @@ contract ImmutableSignedZoneV2 is
)
{
domainSeparator = _domainSeparator();
apiEndpoint = _sip7APIEndpoint;
apiEndpoint = _apiEndpoint;

substandards = _getSupportedSubstandards();

documentationURI = _documentationURI;
}

/**
* @notice ERC-165 interface support.
*
* @param interfaceId The interface ID to check for support.
*/
function supportsInterface(bytes4 interfaceId)
public
view
override(ERC165, ZoneInterface, AccessControlEnumerable)
returns (bool)
{
return interfaceId == type(ZoneInterface).interfaceId || interfaceId == type(SIP5Interface).interfaceId
|| interfaceId == type(SIP7Interface).interfaceId || super.supportsInterface(interfaceId);
}

/**
* @notice Validates a fulfilment execution.
*
* @dev This function is called by Seaport whenever any extraData is
* provided by the caller.
*
* @param zoneParameters The zone parameters containing data related to
the fulfilment execution.
* the fulfilment execution.
* @return validOrderMagicValue A magic value indicating if the order is
* currently valid.
*/
Expand Down Expand Up @@ -299,7 +303,7 @@ contract ImmutableSignedZoneV2 is

// Derive the EIP-712 digest using the domain separator and signedOrder
// hash through openzepplin helper.
bytes32 digest = ECDSA.toTypedDataHash(_domainSeparator(), signedOrderHash);
bytes32 digest = MessageHashUtils.toTypedDataHash(_domainSeparator(), signedOrderHash);

// Recover the signer address from the digest and signature.
// Pass in R and VS from compact signature (ERC2098).
Expand All @@ -315,6 +319,42 @@ contract ImmutableSignedZoneV2 is
validOrderMagicValue = ZoneInterface.validateOrder.selector;
}

/**
* @notice ERC-165 interface support.
*
* @param interfaceId The interface ID to check for support.
*/
function supportsInterface(bytes4 interfaceId)
public
view
override(ERC165, ZoneInterface, AccessControlEnumerable)
returns (bool)
{
return interfaceId == type(ZoneInterface).interfaceId || interfaceId == type(SIP5Interface).interfaceId
|| interfaceId == type(SIP7Interface).interfaceId || super.supportsInterface(interfaceId);
}

/**
* @dev Internal view function to get the EIP-712 domain separator. If the
* chainId matches the chainId set on deployment, the cached domain
* separator will be returned; otherwise, it will be derived from
* scratch.
*
* @return The domain separator.
*/
function _domainSeparator() internal view returns (bytes32) {
return block.chainid == _CHAIN_ID ? _DOMAIN_SEPARATOR : _deriveDomainSeparator();
}

/**
* @dev Internal view function to derive the EIP-712 domain separator.
*
* @return domainSeparator The derived domain separator.
*/
function _deriveDomainSeparator() internal view returns (bytes32 domainSeparator) {
return keccak256(abi.encode(_EIP_712_DOMAIN_TYPEHASH, _NAME_HASH, _VERSION_HASH, block.chainid, address(this)));
}

/**
* @dev Get the supported substandards of the contract.
*
Expand All @@ -339,7 +379,7 @@ contract ImmutableSignedZoneV2 is
*/
function _deriveSignedOrderHash(address fulfiller, uint64 expiration, bytes32 orderHash, bytes calldata context)
internal
view
pure
returns (bytes32 signedOrderHash)
{
// Derive the signed order hash.
Expand Down Expand Up @@ -570,25 +610,4 @@ contract ImmutableSignedZoneV2 is
// All elements from values exist in sourceArray
return true;
}

/**
* @dev Internal view function to get the EIP-712 domain separator. If the
* chainId matches the chainId set on deployment, the cached domain
* separator will be returned; otherwise, it will be derived from
* scratch.
*
* @return The domain separator.
*/
function _domainSeparator() internal view returns (bytes32) {
return block.chainid == _CHAIN_ID ? _DOMAIN_SEPARATOR : _deriveDomainSeparator();
}

/**
* @dev Internal view function to derive the EIP-712 domain separator.
*
* @return domainSeparator The derived domain separator.
*/
function _deriveDomainSeparator() internal view returns (bytes32 domainSeparator) {
return keccak256(abi.encode(_EIP_712_DOMAIN_TYPEHASH, _NAME_HASH, _VERSION_HASH, block.chainid, address(this)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Immutable Signed Zone (v2)

The Immutable Signed Zone contract is a [Seaport Zone](https://docs.opensea.io/docs/seaport-hooks#zone-hooks) that implements [SIP-7 (Interface for Server-Signed Orders)](https://github.com/ProjectOpenSea/SIPs/blob/main/SIPS/sip-7.md) with support for [substandards](https://github.com/ProjectOpenSea/SIPs/blob/main/SIPS/sip-7.md#substandards) 3, 4 and 6.

This zone is used by Immutable to enable:

* Enforcement of protocol, royalty and ecosystem fees
* Off-chain order cancellation

# Status

Contract threat models and audits:

| Description | Date | Version Audited | Link to Report |
| ------------------------------- | ---- | --------------- | -------------- |
| Not audited and no threat model | - | - | - |

## ImmutableSignedZoneV2

| Location | Date | Version Deployed | Address |
| ----------------------- | ------------ | ---------------- | ------- |
| Immutable zkEVM Testnet | Not deployed | - | - |
| Immutable zkEVM Mainnet | Not deployed | - | - |

## Architecture

The trading system on the Immutable platform is shown in the diagram below.

```mermaid
flowchart LR
client[Client] <-- 1. POST .../fulfillment-data ---> ob[Immutable Off-Chain\nOrderbook]
client -- 2. fulfillAdvancedOrder ---> seaport[ImmutableSeaport.sol]
seaport -- 3a. transferFrom --> erc20[IERC20.sol]
seaport -- 3b. transferFrom --> erc721[IERC721.sol]
seaport -- 3c. safeTransferFrom --> erc1155[IERC1155.sol]
seaport -- 4. validateOrder --> zone[ImmutableSignedZoneV2.sol]
```

The sequence of events is as follows:

1. The client makes a HTTP `POST .../fulfillment-data` request to the Immutable Orderbook, which will construct signs and sign an `extraData` payload to return to the client
2. The client calls `fulfillAdvancedOrder` or `fulfillAvailableAdavancedOrders` on `ImmutableSeaport.sol` to fulfill an order
3. `ImmutableSeaport.sol` executes the fufilment by transferring items between parties
4. `ImmutableSeaport.sol` calls `validateOrder` on `ImmutableSignedZoneV2.sol`, passing it the fulfilment execution details as well as the `extraData` parameter
1. `ImmutableSignedZoneV2.sol` validates the fulfilment execution details using the `extraData` payload, reverting if expectations are not met
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ interface SIP7Interface is SIP7EventsAndErrors {
* @dev The struct for storing signer info.
*/
struct SignerInfo {
/// If the signer is currently active.
/// @dev If the signer is currently active.
bool active;
/// If the signer has been active before.
/// @dev If the signer has been active before.
bool previouslyActive;
}

Expand All @@ -46,6 +46,13 @@ interface SIP7Interface is SIP7EventsAndErrors {
*/
function updateAPIEndpoint(string calldata newApiEndpoint) external;

/**
* @notice Update the documentation URI returned by this zone.
*
* @param newDocumentationURI The new documentation URI.
*/
function updateDocumentationURI(string calldata newDocumentationURI) external;

/**
* @notice Returns signing information about the zone.
*
Expand Down
8 changes: 4 additions & 4 deletions test/trading/seaport/ImmutableSeaportHarness.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,17 @@ import {ImmutableSeaport} from "../../../contracts/trading/seaport/ImmutableSeap
contract ImmutableSeaportHarness is ImmutableSeaport {
constructor(address conduitController, address owner) ImmutableSeaport(conduitController, owner) {}

function exposed_domainSeparator() external view returns (bytes32) {
return _domainSeparator();
}

function exposed_deriveEIP712Digest(bytes32 domainSeparator, bytes32 orderHash)
external
pure
returns (bytes32 value)
{
return _deriveEIP712Digest(domainSeparator, orderHash);
}

function exposed_domainSeparator() external view returns (bytes32) {
return _domainSeparator();
}
}

// solhint-enable func-name-mixedcase
Loading
Loading