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 + 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 initializer
Sets 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 = 37
INVOKE_WALLET_FLAG = 20
Entry Point
function runAction(CircomData circomData, int256[] deltaAmountChanges)
external onlyAllowedRecipient returns (UTXO[] utxoSet)
High-level flow:
Decode
EmporiumStack
fromcircomData.externalActionMetadata
.Snapshot
balancesBefore
for all(erc20TokenAddresses, tokenIds)
.verifyWallet(stack)
— if any op is stateful, verify EIP-712 signature and anti-replay.Execute ops in order:
If
invokeWallet
andsignerAddress != 0
: callIHinkalWallet.callHinkalWallet(endpoint, callData, value)
.Else: direct
endpoint.call{value: value}(callData)
with guard to block wallet-only methods.Revert on failure bubbling
err
asCallFailed(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 matchingUTXO
withstealthAddressStructure
andtimeStamp
.
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) internal
Detects if any op requests
invokeWallet
with 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
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 andTransferer
helpers.
Last updated