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,EIP712UpgradeableExternalActionBaseUpgradeable— recipient allowlist + ownershipTransferer— token and ETH helpersEmporiumStorage— upgrader-safe storage layout
Storage
IHinkalHelper _hinkalHelper— for relay fee policy and helper accessmapping(uint256 => bool) usedMessages— anti-replay for signed stacks
Initialization
function initialize(IHinkalHelper _hinkalHelper, address[] _allowedRecipients, address _owner) public initializerSets EIP712 domain: name
Emporium, version1.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 addressdata[20]: invokeWallet flag (nonzero → use IHinkalWallet)data[21:37]: value (uint128)data[37:]: callData for endpoint
Constants:
MIN_DATA_LENGTH = 37INVOKE_WALLET_FLAG = 20
Entry Point
function runAction(CircomData circomData, int256[] deltaAmountChanges)
external onlyAllowedRecipient returns (UTXO[] utxoSet)High-level flow:
Decode
EmporiumStackfromcircomData.externalActionMetadata.Snapshot
balancesBeforefor all(erc20TokenAddresses, tokenIds).verifyWallet(stack)— if any op is stateful, verify EIP-712 signature and anti-replay.Execute ops in order:
If
invokeWalletandsignerAddress != 0: callIHinkalWallet.callHinkalWallet(endpoint, callData, value).Else: direct
endpoint.call{value: value}(callData)with guard to block wallet-only methods.Revert on failure bubbling
errasCallFailed(err).
Snapshot
balancesAfter.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 matchingUTXOwithstealthAddressStructureandtimeStamp.
Pay relay fee:
If
signerAddress != 0:IHinkalWallet.doSendToRelay(relay, flatFee, feeToken).Else:
sendToRelay(relay, flatFee, feeToken).
Return compacted
utxoSet.
Reverts:
ShortOperation()— op shorter than 37 bytesUnauthorizedWalletCall()— attempt to call wallet-only methods directlyCallFailed(bytes err)— bubbled revert dataBalanceChangeShouldBePositive()— net outflow detectedInvalidSignature()/UsedMessage()— EIP-712 checks failed
Wallet Verification
function verifyWallet(EmporiumStack stack) internalDetects if any op requests
invokeWalletwith nonzerosignerAddress.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
deltaAmountChangesand 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 andTransfererhelpers.
Last updated