05 June 2025
We're excited to announce Story's upgrade to maintain maximum compatibility with the latest EVM ecosystem. We're upgrading our Geth-based execution environment to incorporate important changes from the recent Ethereum Pectra hard fork. This upgrade reflects our commitment to maintaining an efficient, fully compatible execution layer while providing developers with new capabilities.
The Pectra upgrade introduces significant changes, including enhancements in account abstraction, validator operations, and network performance. This is done through several EIPs that enhance user/developer experience, improve performance, and increase network resiliency. While some EIPs focus on consensus and staking parameters, these are not directly applicable to Story, as our powerful consensus engine already incorporates many of these features.
Given these significant changes to the user experience, we aim to be among the first blockchains to incorporate them while maintaining maximum user safety. Thus, over the past few months, we have planned, reviewed, and prepared a series of phased upgrades with rigorous and comprehensive testing, prioritizing changes that deliver the greatest value to Story users and developers. Here are the key updates included in the first network upgrade:
EIP-7702 – Set codes for EOAs
This EIP introduces a new transaction type that enables lightweight account abstraction, supporting features like batching, spend limits, and off-chain authorization—all without needing a full smart contract wallet.
EIP-2537 – Precompile for BLS12-381 curve operations
This EIP adds native support for BLS12-381 curve operations, enabling efficient BLS signature verification in the EVM — a major win for ZK systems, light clients, and staking.
EIP-7623 – Increase calldata cost
This EIP increases calldata cost per byte (from 4/16 to 10/40 gas), reducing the maximum payload size per block (from ~8.6MB to ~3.4MB). This discourages abuse from high-volume data transactions without affecting standard DeFi, social, or bridging use cases.
These updates are currently deployed on Aeneid Testnet and will soon be on Mainnet. As part of the next update, we are deploying additional EIPs, including EIP-2925, and combining EIP-7702 with our current Web2-friendly registration system built on ERC-4337. This integration will provide a smoother onboarding experience while improving our control over execution, gas abstraction, and security rules.
To better demonstrate how EIP-7702 can dramatically improve user experience, here's a practical demo of its capabilities.
We use a simple Counter.sol
contract that supports signature-based access control. This allows EOAs to temporarily act like a smart contract when used with EIP-7702.
In a real-world use case, such as with smart contract wallets (e.g., MetaMask Snaps or Safe modules), the logic would be much more complex, unlocking rich features like batching, permissions, and meta-transactions. This simple example is meant to demonstrate how EIP-7702 works in principle.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;
contract Counter {
uint256 public number;
bool public initialized;
modifier onlyInitialized() {
require(initialized, "Not initialized");
_;
}
/// Initializes the contract. Can only be called once.
/// The signature must be over keccak256(abi.encodePacked("initialize", initialValue))
function initialize(uint256 initialValue, bytes calldata signature) external {
require(!initialized, "Already initialized");
bytes32 hash = keccak256(abi.encodePacked("initialize", initialValue));
bytes32 ethSignedHash = keccak256(
abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)
);
(bytes32 r, bytes32 s, uint8 v) = splitSignature(signature);
address signer = ecrecover(ethSignedHash, v, r, s);
require(signer == address(this), "Invalid signature");
number = initialValue;
initialized = true;
}
/// Sets a new value. Requires a valid signature.
/// The signature must be over keccak256(abi.encodePacked("setNumber", newValue))
function setNumber(uint256 newValue, bytes calldata signature) external onlyInitialized {
bytes32 hash = keccak256(abi.encodePacked("setNumber", newValue));
bytes32 ethSignedHash = keccak256(
abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)
);
(bytes32 r, bytes32 s, uint8 v) = splitSignature(signature);
address signer = ecrecover(ethSignedHash, v, r, s);
require(signer == address(this), "Invalid signature");
number = newValue;
}
/// Returns the current value
function getNumber() external view returns (uint256) {
return number;
}
/// Splits a 65-byte signature into (r, s, v)
function splitSignature(bytes memory sig)
internal
pure
returns (bytes32 r, bytes32 s, uint8 v)
{
require(sig.length == 65, "Invalid signature length");
assembly {
r := mload(add(sig, 0x20))
s := mload(add(sig, 0x40))
v := byte(0, mload(add(sig, 0x60)))
}
}
}
We'll now walk through how to use EIP-7702 to deploy this contract and delegate its logic to an EOA account.
Sample configuration:
# Story Aeneid Testnet RPC: <https://aeneid.storyrpc.io>
ETH_RPC_URL=http://localhost:8545
PRIVATE_KEY= PRIKEY_EOA_1
ACCOUNT= ADDR_EOA_1
Use Foundry to deploy the Counter
contract.
You may need to adjust gas-price
, priority-gas-price
, and gas-limit
depending on your chain's state and the contract complexity.
# 1.1 Initialize a new Foundry project
forge init foundry && cd foundry
# 1.2 Replace src/Counter.sol with the above demo contract
# 1.3 Deploy the contract
forge create --rpc-url $ETH_RPC_URL \
--private-key $PRIKEY_EOA_1 \
src/Counter.sol:Counter \
--broadcast \
--gas-price 100gwei \
--priority-gas-price 20gwei \
--gas-limit 1000000
Sample output:
Deployer: ADDR_EOA_1
Deployed to: ADDR_CONTRACT
Transaction hash: 0x347ba....5ea1
Generate a new EOA using cast wallet new
.
Sample output:
Address: ADDR_EOA_2
Private key: PRIKEY_EOA_2
The EOA_2 doesn't need to have a positive balance.
Use the new EOA to authorize the deployed contract code as its runtime logic:
cast wallet sign-auth ADDR_CONTRACT \
--private-key PRIKEY_EOA_2
This returns the AUTH
payload. Now send a setCodeTx with EOA_1 to set the delegation:
cast send $(cast az) \
--private-key $PRIKEY_EOA_1 \
--auth AUTH
Confirm the EOA_2 now has deployed contract logic:
# Expected output: 0xef0100+ADDR_CONTRACT
cast code ADDR_EOA_2 \
--rpc-url $ETH_RPC_URL
# Expected output: 1
cast nonce ADDR_EOA_2 \
--rpc-url $ETH_RPC_URL
# Expected output: 0x0000000000000000000000000000000000000000000000000000000000000000. It means not initialized.
cast call ADDR_EOA_2 "getNumber()" --rpc-url ETH_RPC_URL
Under EIP-7702, code is set on an EOA without executing initcode, which means you can't initialize storage during deployment. To avoid front-running attacks — where an observer could initialize the account with malicious values — developers should require the initial setup call to be signed by the original EOA and verified via ecrecover
. This ensures only the intended initialization is accepted.
Here’s an example shell script to initialize the contract. Replace the variables as needed:
#!/bin/bash
set -e
INIT_VALUE=1
PRIKEY_EOA_2=$PRIKEY_EOA_2
ADDR_EOA_2=$ADDR_EOA_2
RPC_URL=$RPC_URL
CHAIN_ID=$CHAIN_ID
PRIKEY_EOA_1=$PRIKEY_EOA_1
# construct hash
METHOD_HEX=$(echo -n "initialize" | xxd -p -c256)
VALUE_HEX=$(printf "%064x" $INIT_VALUE)
PACKED_HEX="${METHOD_HEX}${VALUE_HEX}"
HASH=$(cast keccak256 "0x$PACKED_HEX")
# sign hash
SIG=$(cast wallet sign --private-key $PRIKEY_EOA_2 $HASH)
echo "Signature: $SIG"
# call initialize(uint256, bytes)
echo "Calling initialize..."
cast send $ADDR_EOA_2 "initialize(uint256,bytes)" $INIT_VALUE $SIG \
--rpc-url $RPC_URL \
--private-key $PRIKEY_EOA_1 \
--chain $CHAIN_ID \
--gas-price 100gwei --priority-gas-price 20gwei --gas-limit 1000000
Confirm the EOA_2 has been initialized successfully:
# Expected output: 0x0000000000000000000000000000000000000000000000000000000000000001.
cast call ADDR_EOA_2 "getNumber()" --rpc-url RPC_URL
Once initialized, you can interact with EOA_2 like any smart contract. This is similar to the initialization step, but now calling other contract methods.
Here's a shell script to call setNumber()
:
#!/bin/bash
set -e
NEW_VALUE=2
PRIKEY_EOA_2=$PRIKEY_EOA_2
ADDR_EOA_2=$ADDR_EOA_2
RPC_URL=$RPC_URL
CHAIN_ID=$CHAIN_ID
PRIKEY_EOA_1=$PRIKEY_EOA_1
METHOD_HEX=$(echo -n "setNumber" | xxd -p -c256)
VALUE_HEX=$(printf "%064x" $NEW_VALUE)
PACKED_HEX="${METHOD_HEX}${VALUE_HEX}"
HASH=$(cast keccak "0x$PACKED_HEX")
SIG=$(cast wallet sign --private-key $PRIKEY_EOA_2 $HASH)
echo "Calling setNumber..."
cast send $ADDR_EOA_2 "setNumber(uint256,bytes)" $NEW_VALUE $SIG \
--rpc-url $RPC_URL \
--private-key $PRIKEY_EOA_1 \
--chain $CHAIN_ID \
--gas-price 100gwei --priority-gas-price 20gwei --gas-limit 1000000