Emporium

Overview

EmporiumUpgradeable is an upgradeable external action that executes a stack of arbitrary calls (stateless or via a stateful Hinkal Wallet) and returns newly formed UTXOs back to Hinkal. It supports:

  • Multi-operation batches with per-op value and calldata

  • Optional stateful execution through a user’s Hinkal Wallet contract

  • EIP-712 signed authorization for stateful sequences (anti-replay via message nonce)

  • Accurate token/NFT accounting, fee payment to relay, and UTXO formation

Inheritance

  • Initializable, EIP712Upgradeable

  • ExternalActionBaseUpgradeable — recipient allowlist + ownership

  • Transferer — token and ETH helpers

  • EmporiumStorage — upgrader-safe storage layout

Storage

  • IHinkalHelper _hinkalHelper — for relay fee policy and helper access

  • mapping(uint256 => bool) usedMessages — anti-replay for signed stacks

Initialization

function initialize(IHinkalHelper _hinkalHelper, address[] _allowedRecipients, address _owner) public initializer
  • Sets EIP712 domain: name Emporium, version 1.0.0.

  • Configures allowed recipients and owner, stores _hinkalHelper.


Metadata Format — EmporiumStack

struct EmporiumStack {
    uint8 v; bytes32 r; bytes32 s;    // signature over message
    uint256 message;                  // signed nonce/message
    address signerAddress;            // wallet address (optional)
    bytes[] ops;                      // array of operations
}

Each op packs:

  • data[0:20]: endpoint address

  • data[20]: invokeWallet flag (nonzero → use IHinkalWallet)

  • data[21:37]: value (uint128)

  • data[37:]: callData for endpoint

Constants:

  • MIN_DATA_LENGTH = 37

  • INVOKE_WALLET_FLAG = 20


Entry Point

function runAction(CircomData circomData, int256[] deltaAmountChanges)
    external onlyAllowedRecipient returns (UTXO[] utxoSet)

High-level flow:

  1. Decode EmporiumStack from circomData.externalActionMetadata.

  2. Snapshot balancesBefore for all (erc20TokenAddresses, tokenIds).

  3. verifyWallet(stack) — if any op is stateful, verify EIP-712 signature and anti-replay.

  4. Execute ops in order:

    • If invokeWallet and signerAddress != 0: call IHinkalWallet.callHinkalWallet(endpoint, callData, value).

    • Else: direct endpoint.call{value: value}(callData) with guard to block wallet-only methods.

    • Revert on failure bubbling err as CallFailed(err).

  5. Snapshot balancesAfter.

  6. Per-token accounting and UTXO formation:

    • Start with balanceChange = balancesAfter[i] - balancesBefore[i].

    • If deltaAmountChanges[i] < 0, add -deltaAmountChanges[i] to account for pre-move into Emporium.

    • Revert if balanceChange < 0 (unless pre-balance existed, which is prohibited here).

    • If positive, transferToken(token, msg.sender, balanceChange, tokenId) and create matching UTXO with stealthAddressStructure and timeStamp.

  7. Pay relay fee:

    • If signerAddress != 0: IHinkalWallet.doSendToRelay(relay, flatFee, feeToken).

    • Else: sendToRelay(relay, flatFee, feeToken).

  8. Return compacted utxoSet.

Reverts:

  • ShortOperation() — op shorter than 37 bytes

  • UnauthorizedWalletCall() — attempt to call wallet-only methods directly

  • CallFailed(bytes err) — bubbled revert data

  • BalanceChangeShouldBePositive() — net outflow detected

  • InvalidSignature() / UsedMessage() — EIP-712 checks failed


Wallet Verification

function verifyWallet(EmporiumStack stack) internal
  • Detects if any op requests invokeWallet with nonzero signerAddress.

  • If none, return early (stateless batch).

  • For stateful:

    • Enforce anti-replay: usedMessages[stack.message] must be false; mark true.

    • EIP-712 hash over EmporiumSignature(uint256 message).

    • Recover signer and ensure it matches stack.signerAddress.


Integration with Hinkal

  • Hinkal pre-moves tokens based on deltaAmountChanges and delegates here.

  • Emporium executes ops, returns UTXOs representing net positive balances per token.

  • Hinkal applies slippage/balance invariants and inserts commitments accordingly.


Security Notes

  • Recipient gating: only Hinkal may invoke runAction.

  • Strong EIP-712 binding for stateful sequences; replay-protection via usedMessages.

  • Prohibits direct invocation of wallet-reserved selectors.

  • Careful ETH handling via value (uint128) per op and Transferer helpers.

Last updated