Session keys with cast
This guide shows how to test an EIP-7702 session key flow with Foundry's cast.
It uses a minimal delegate contract that lets an owner EOA authorize one session key to deploy a
specific contract bytecode.
What this verifies
- An EOA can delegate to account code with an EIP-7702 type
0x04transaction. - The delegated code can store a scoped session key in the EOA's storage.
- The session key can later call the EOA and deploy allowed bytecode from the delegated account code.
- Eden can sponsor that later session-key call with a type
0x76transaction, where the session key is the executor and the app gas payer is the fee payer.
cast is enough for the EIP-7702 setup and the unsponsored session-key transaction. For the
gas-sponsored transaction, use an Eden-compatible ev-reth client. Current Foundry cast --tempo
builds a Tempo-flavored type 0x76 payload, not the ev-reth EvNodeTransaction format Eden uses
for batching and fee-payer sponsorship.
Verified Eden testnet transactions
These transactions were produced from the Eden testnet command sequence in this guide:
- Install 7702 delegation and session grant: type
0x04. - Deploy with the session key: unsponsored session-key call.
- Deploy with the sponsored session key: Eden type
0x76, with the session key as executor and the sponsor asfeePayer.
Prerequisites
- Foundry installed with
EIP-7702 support for
cast send --auth. jqfor reading Forge artifact JSON.- The example contracts in
examples/session-keys-cast. - For local testing, run Anvil with Prague enabled.
- For Eden gas sponsorship, the
@evstack/evnode-viempackage.
anvil --hardfork prague --chain-id 714In another terminal, set the local test accounts:
export RPC_URL=http://127.0.0.1:8545
export OWNER_PK=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
export SESSION_PK=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
export OWNER=$(cast wallet address --private-key $OWNER_PK)
export SESSION=$(cast wallet address --private-key $SESSION_PK)Deploy the delegate implementation
Deploy the code the owner EOA will delegate to:
forge create examples/session-keys-cast/EdenSessionAccount.sol:EdenSessionAccount \
--rpc-url $RPC_URL \
--private-key $OWNER_PK \
--broadcastSave the deployed implementation address:
export DELEGATE_IMPL=<0x...deployedTo>Build the deploy target
Compile the example target and hash its creation bytecode. The session key will only be able to deploy bytecode matching this hash.
forge build --contracts examples/session-keys-cast
export INIT_CODE=$(jq -r '.bytecode.object | if startswith("0x") then . else "0x" + . end' \
out/SessionKeyDeployTarget.sol/SessionKeyDeployTarget.json)
export INIT_CODE_HASH=$(cast keccak "$INIT_CODE")
export EXPIRY=4102444800Install the 7702 delegation and session key
This sends an EIP-7702 type 0x04 transaction. The --auth flag authorizes the owner EOA to
delegate to DELEGATE_IMPL, and the transaction calls the owner EOA to install the session grant.
cast send $OWNER \
"installSession(address,uint64,bytes32,uint256)" \
$SESSION \
$EXPIRY \
$INIT_CODE_HASH \
2 \
--auth $DELEGATE_IMPL \
--rpc-url $RPC_URL \
--private-key $OWNER_PK \
--gas-limit 1000000Verify that the EOA now has a delegation indicator and the session grant is stored:
cast code $OWNER --rpc-url $RPC_URL
cast call $OWNER "sessionKey()(address)" --rpc-url $RPC_URL
cast call $OWNER "deploysRemaining()(uint256)" --rpc-url $RPC_URL
cast call $OWNER "allowedInitCodeHash()(bytes32)" --rpc-url $RPC_URLThe code should start with 0xef0100, followed by the delegate implementation address.
Deploy with the session key
The session key now calls the owner EOA. The delegated account code checks the session policy and deploys the approved bytecode internally.
cast send $OWNER \
"deploy(bytes)" \
"$INIT_CODE" \
--rpc-url $RPC_URL \
--private-key $SESSION_PK \
--gas-limit 1000000Check the deployed contract:
export DEPLOYED=$(cast call $OWNER "lastDeployment()(address)" --rpc-url $RPC_URL)
cast code $DEPLOYED --rpc-url $RPC_URL
cast call $DEPLOYED "meaning()(uint256)" --rpc-url $RPC_URL
cast call $OWNER "deploysRemaining()(uint256)" --rpc-url $RPC_URLThe meaning() call should return 42. Because the install command above grants 2 deploys,
deploysRemaining() should return 1 after this unsponsored deploy.
Sponsor the session-key call on Eden
For gas-sponsored UX on Eden, do not move this flow to legacy account abstraction. Keep the same
7702 session-key account code, and send the session-key call as an Eden type 0x76 transaction:
- executor: the session key
- sponsor: the app gas payer
- call target: the owner EOA
- calldata:
deploy(bytes)or another session-approved function
The TypeScript below uses the ev-reth client shape. It signs the call with the session key, adds a
fee-payer signature from the sponsor, and submits the serialized type 0x76 transaction with
eth_sendRawTransaction.
In a scratch package or your app, install the client dependencies:
npm install viem @evstack/evnode-viem
npm install --save-dev tsxSet the sponsor key before running the script. On testnet, this can be any funded throwaway key. In production, this should be the app gas payer.
export SPONSOR_PK=<app-gas-payer-private-key>Save this as sponsored-session.ts:
import { createClient, encodeFunctionData, http, parseAbi, type Address, type Hex } from 'viem'
import { privateKeyToAccount, sign } from 'viem/accounts'
import { createEvnodeClient } from '@evstack/evnode-viem'
const rpcUrl = process.env.RPC_URL!
const owner = process.env.OWNER as Address
const initCode = process.env.INIT_CODE as Hex
const sessionPk = process.env.SESSION_PK as Hex
const sponsorPk = process.env.SPONSOR_PK as Hex
const client = createClient({ transport: http(rpcUrl) })
const session = privateKeyToAccount(sessionPk)
const sponsor = privateKeyToAccount(sponsorPk)
const evnode = createEvnodeClient({
client,
executor: {
address: session.address,
signHash: async (hash) => sign({ hash, privateKey: sessionPk }),
},
sponsor: {
address: sponsor.address,
signHash: async (hash) => sign({ hash, privateKey: sponsorPk }),
},
})
const abi = parseAbi(['function deploy(bytes initCode)'])
const data = encodeFunctionData({
abi,
functionName: 'deploy',
args: [initCode],
})
const intent = await evnode.createIntent({
calls: [{ to: owner, value: 0n, data }],
gasLimit: 1_000_000n,
})
console.log(await evnode.sponsorAndSend({ intent }))Run the script and save the printed transaction hash:
npx tsx sponsored-session.ts
export SPONSORED_TX=<0x...printed-hash>Then use cast to inspect and verify the sponsored transaction:
cast tx $SPONSORED_TX --rpc-url $RPC_URL
export DEPLOYED=$(cast call $OWNER "lastDeployment()(address)" --rpc-url $RPC_URL)
cast call $DEPLOYED "meaning()(uint256)" --rpc-url $RPC_URL
cast call $OWNER "deploysRemaining()(uint256)" --rpc-url $RPC_URLThe transaction should show type 0x76, from as the session key, and feePayer as the sponsor.
The meaning() call should return 42.
Revoke the session
Only the owner EOA can revoke because revokeSession() requires msg.sender == address(this).
cast send $OWNER \
"revokeSession()" \
--rpc-url $RPC_URL \
--private-key $OWNER_PK \
--gas-limit 1000000Eden testnet example commands
This is the same flow as above, written as one Eden testnet command sequence. It uses three throwaway keys:
OWNER_PK: the EOA that will receive the 7702 delegation.SESSION_PK: the scoped session key.SPONSOR_PK: the app gas payer for the sponsored0x76transaction.
Generate keys, set the Eden testnet RPC, and print the addresses:
export RPC_URL=https://rpc.testnet.eden.gateway.fm/
cast wallet new
cast wallet new
cast wallet new
export OWNER_PK=<first-private-key>
export SESSION_PK=<second-private-key>
export SPONSOR_PK=<third-private-key>
export OWNER=$(cast wallet address --private-key $OWNER_PK)
export SESSION=$(cast wallet address --private-key $SESSION_PK)
export SPONSOR=$(cast wallet address --private-key $SPONSOR_PK)
printf "OWNER=%s\nSESSION=%s\nSPONSOR=%s\n" "$OWNER" "$SESSION" "$SPONSOR"Fund OWNER, SESSION, and SPONSOR from the Eden testnet faucet, then
check balances and nonces:
cast balance $OWNER --rpc-url $RPC_URL --ether
cast balance $SESSION --rpc-url $RPC_URL --ether
cast balance $SPONSOR --rpc-url $RPC_URL --ether
cast nonce $OWNER --rpc-url $RPC_URL
cast nonce $SESSION --rpc-url $RPC_URL
cast nonce $SPONSOR --rpc-url $RPC_URLDeploy the delegate implementation:
export DELEGATE_IMPL=$(forge create \
examples/session-keys-cast/EdenSessionAccount.sol:EdenSessionAccount \
--rpc-url $RPC_URL \
--private-key $OWNER_PK \
--broadcast \
--json | jq -r '.deployedTo')
echo $DELEGATE_IMPLBuild the deploy target and install the session grant with a type 0x04 EIP-7702 transaction:
forge build --contracts examples/session-keys-cast
export INIT_CODE=$(jq -r '.bytecode.object | if startswith("0x") then . else "0x" + . end' \
out/SessionKeyDeployTarget.sol/SessionKeyDeployTarget.json)
export INIT_CODE_HASH=$(cast keccak "$INIT_CODE")
export EXPIRY=4102444800
cast send $OWNER \
"installSession(address,uint64,bytes32,uint256)" \
$SESSION \
$EXPIRY \
$INIT_CODE_HASH \
2 \
--auth $DELEGATE_IMPL \
--rpc-url $RPC_URL \
--private-key $OWNER_PK \
--gas-limit 1000000Verify the delegated EOA and installed session:
cast code $OWNER --rpc-url $RPC_URL
cast call $OWNER "sessionKey()(address)" --rpc-url $RPC_URL
cast call $OWNER "allowedInitCodeHash()(bytes32)" --rpc-url $RPC_URL
cast call $OWNER "deploysRemaining()(uint256)" --rpc-url $RPC_URLSend one unsponsored session-key deploy with cast:
cast send $OWNER \
"deploy(bytes)" \
"$INIT_CODE" \
--rpc-url $RPC_URL \
--private-key $SESSION_PK \
--gas-limit 1000000
export DEPLOYED=$(cast call $OWNER "lastDeployment()(address)" --rpc-url $RPC_URL)
cast call $DEPLOYED "meaning()(uint256)" --rpc-url $RPC_URL
cast call $OWNER "deploysRemaining()(uint256)" --rpc-url $RPC_URLSend the second deploy as a sponsored Eden 0x76 transaction with the sponsored-session.ts
script above:
npx tsx sponsored-session.ts
export SPONSORED_TX=<0x...printed-hash>
cast tx $SPONSORED_TX --rpc-url $RPC_URL
export DEPLOYED=$(cast call $OWNER "lastDeployment()(address)" --rpc-url $RPC_URL)
cast call $DEPLOYED "meaning()(uint256)" --rpc-url $RPC_URL
cast call $OWNER "deploysRemaining()(uint256)" --rpc-url $RPC_URLDo not use Foundry's current cast --tempo flags for Eden sponsorship unless the payload format has
been confirmed against ev-reth. In the Foundry build tested for this guide, cast --tempo did not
match Eden's EvNodeTransaction fields.