Frame Transactions
FrameAccount was a variant of the NiceTry account that expressed the same ephemeral-key rule natively, using EIP-8141 frame transactions instead of ERC-4337. It authenticated with the same ForsVerifier and enforced the same invariant (every authorised transaction must rotate the signer) but did so without an EntryPoint or UserOperation.
What frame transactions are
EIP-8141 introduces a transaction type built from a sequence of frames, each with its own mode, target, value, gas limit, and calldata:
tx = { sender, nonce, frames: [ [mode, flags, target, gas, value, data], ... ], fee fields }The modes relevant here:
| Mode | Meaning |
|---|---|
VERIFY | validate the transaction (static: no state changes; may APPROVE execution) |
SENDER | execute as the transaction sender, after approval |
DEFAULT | execute through the frame entry context |
The point for NiceTry: account validation becomes native. A smart account defines its own validation in a VERIFY frame and inspects later frames through introspection opcodes (TXPARAM, FRAMEPARAM, FRAMEDATALOAD). No ERC-4337 validateUserOp is required.
Why rotation must be a separate frame
In ERC-4337, rotation can happen inside validateUserOp because validation may write state. A VERIFY frame cannot: it is static. If the account merely checked the FORS signature and called APPROVE, it would authorise execution without forcing rotation, breaking the ephemeral-key invariant.
So the frame account split the two and made rotation a validation requirement:
Frame 0: VERIFY -> check FORS signature, then require Frame 1 to be the rotation
Frame 1: SENDER -> call rotateOwner(nextOwner)
Frame 2+: SENDER -> user actions (run only after rotation)The security invariant in one line:
No frame transaction is approved unless signer rotation is already scheduled as the very next frame.
How FrameAccount enforced it
The VERIFY frame arrived as a fallback whose calldata was the FORS signature blob:
fallback() external {
_validateFrameSignature(msg.data); // recover signer, require next frame rotates
_approveExecutionAndPayment();
}
function rotateOwner(address nextOwner) external {
_requireSelf(); // only callable by the account itself, via the SENDER frame
_rotateOwner(nextOwner);
}_validateFrameSignature recovered the signer over _txSigHash() and checked it against owner, then required the next frame to be exactly a self-call to rotateOwner:
mode == SENDER
target == address(this)
value == 0
atomic == false (not an atomic-batch frame)
dataLength == 36 (4-byte selector + 32-byte address)
selector == rotateOwner(address)
nextOwner != address(0)Each failed check reverted with a specific error (FrameAccountMissingRotationFrame, FrameAccountRotationFrameWrongMode, …WrongTarget, …NonZeroValue, …Atomic, …WrongDataLength, …WrongSelector, …ZeroOwner). Only when all passed did the account approve execution and payment.
This preserved the key property of the ERC-4337 design: even if a later user frame fails, rotation has already happened. The exposed FORS key is never left as the active key for a future transaction. The account did not trust client discipline; it refused to approve a transaction that did not schedule rotation next.
Why it stayed abstract
FrameAccount was an abstract contract. The frame-introspection hooks (_txSigHash, _frameCount, _currentFrameIndex, _frameTarget, _frameMode, _frameValue, _frameAtomicBatch, _frameDataLength, _frameDataLoad, _approveExecutionAndPayment) were left unimplemented. They correspond to the draft EIP-8141 opcodes (TXPARAM, FRAMEPARAM, FRAMEDATALOAD, APPROVE), which current solc inline assembly does not expose. The account logic was complete and tested against mocked hooks; only the opcode bridge was missing, and a production deployment would have needed a frame-aware runtime or proxy layer.
The frame account was also not a generic paymaster: its validation path was bound to rotating its own owner, so it could self-pay when it was the sender but could not sponsor other users' frame transactions.
Background: the repo's docs/nicetry-frame-transactions-article.md and the ethresear.ch writeup.