EIP-7702를 활용한 계정 추상화
이 가이드는 EOA에 EIP-7702를 적용하고 위임을 사용하여 세 가지 패턴, 즉 일괄 결제, 지출 한도, 세션 키를 구현하는 과정을 단계별로 설명합니다. EOA는 그 과정 내내 자신의 주소와 개인 키를 유지합니다.
사전 요구 사항
- EOA와 스마트 컨트랙트 계정의 차이 이해(EOA는 기본적으로 코드가 없습니다).
- EVM 트랜잭션 유형에 대한 이해(EIP-2718).
개요
EIP-7702는 authorizationList를 담는 새로운 트랜잭션 유형(0x04)을 도입합니다. 각 권한 부여는 EOA가 해당 트랜잭션에서 실행할 코드를 가진 스마트 컨트랙트를 지정합니다. 흐름은 다음과 같습니다:
- 위임 컨트랙트 선택 또는 배포: EOA가 사용할 로직을 구현하는 표준 Solidity 컨트랙트입니다. 배포된 컨트랙트를 사용하거나 직접 배포할 수 있습니다. 가능하면 감사받은 컨트랙트를 사용하세요.
- 권한 부여 서명: EOA 소유자가 위임 컨트랙트를 승인하는 메시지에 서명합니다.
- EIP-7702 트랜잭션 제출: 트랜잭션에 권한 부여가 포함되며, EOA는 실행 중에 위임 컨트랙트의 코드를 실행합니다.
사용 사례: 일괄 트랜잭션
아래 단계는 Multicall3을 위임 컨트랙트로 사용하여 이 흐름을 단계별로 설명합니다. Multicall3은 여러 호출을 단일 트랜잭션으로 집계하는 널리 배포된 유틸리티 컨트랙트입니다. Multicall3을 EIP-7702 위임 대상으로 지정하면 EOA가 임의의 컨트랙트 상호작용(토큰 전송, 승인, 컨트랙트 읽기, 또는 이들의 조합)을 하나의 원자적 트랜잭션으로 일괄 처리할 수 있습니다. 일괄 결제는 그 한 가지 예입니다. 급여 지급을 위해 열 개의 별도 트랜잭션을 보내는 대신, EOA는 이를 한 번에 모두 실행합니다.
1단계: Multicall3을 위임 컨트랙트로 사용
Multicall3은 Stable에서 0xcA11bde05977b3631167028862bE2a173976CA11에 배포되어 있습니다. 이미 배포되어 널리 사용되고 있으므로 직접 위임 컨트랙트를 배포할 필요가 없습니다. EIP-7702 권한 부여에 서명하면 위임 대상에 EOA에 대한 완전한 실행 권한이 부여됩니다.
// Multicall3 interface (relevant functions only)
interface IMulticall3 {
struct Call3 {
address target;
bool allowFailure;
bytes callData;
}
struct Result {
bool success;
bytes returnData;
}
/// @notice Aggregate calls, allowing each to succeed or fail independently
function aggregate3(Call3[] calldata calls)
external payable returns (Result[] memory returnData);
}2단계: 권한 부여 서명
EOA 소유자가 위임 컨트랙트를 지정하는 권한 부여에 서명합니다. 이 권한 부여는 EIP-7702 트랜잭션에 포함됩니다.
// config.ts
import { ethers } from "ethers";
export const STABLE_TESTNET_RPC = "https://rpc.testnet.stable.xyz";
export const STABLE_TESTNET_CHAIN_ID = 2201;
export const USDT0_ADDRESS = "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9";
export const DELEGATE_ADDRESS = "0xcA11bde05977b3631167028862bE2a173976CA11";
export const provider = new ethers.JsonRpcProvider(STABLE_TESTNET_RPC);
export const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);// signAuthorization.ts
import { ethers } from "ethers";
import { DELEGATE_ADDRESS, STABLE_TESTNET_CHAIN_ID, provider, wallet } from "./config";
export async function signAuthorization() {
const authorization = {
chainId: STABLE_TESTNET_CHAIN_ID,
address: DELEGATE_ADDRESS,
nonce: await provider.getTransactionCount(wallet.address),
};
return wallet.signAuthorization(authorization);
}3단계: EIP-7702 트랜잭션 제출
권한 부여를 Multicall3.aggregate3 호출과 결합합니다. 이 예시는 세 개의 USDT0 전송을 단일 트랜잭션으로 일괄 처리합니다.
import { ethers } from "ethers";
import { wallet, USDT0_ADDRESS } from "./config";
import { signAuthorization } from "./signAuthorization";
const usdt0Interface = new ethers.Interface([
"function transfer(address to, uint256 amount)",
]);
const batchInterface = new ethers.Interface([
"function aggregate3((address target, bool allowFailure, bytes callData)[] calls) returns ((bool success, bytes returnData)[])",
]);
async function main() {
const recipients = [
{ to: "0xAlice...", amount: ethers.parseUnits("100", 6) },
{ to: "0xBob...", amount: ethers.parseUnits("200", 6) },
{ to: "0xCarol...", amount: ethers.parseUnits("150", 6) },
];
const batchData = batchInterface.encodeFunctionData("aggregate3", [
recipients.map(({ to, amount }) => ({
target: USDT0_ADDRESS,
allowFailure: false,
callData: usdt0Interface.encodeFunctionData("transfer", [to, amount]),
})),
]);
const signedAuth = await signAuthorization();
const tx = await wallet.sendTransaction({
type: 4, // EIP-7702 transaction type
to: wallet.address, // call is directed at the EOA itself
data: batchData, // aggregate3 call to execute
authorizationList: [signedAuth],
maxPriorityFeePerGas: 0n,
});
const receipt = await tx.wait(1);
console.log("Batch transactions executed in tx:", receipt.hash);
}Batch transactions executed in tx: 0x...EOA는 Multicall3.aggregate3을 통해 세 개의 호출을 모두 단일 원자적 트랜잭션으로 실행합니다. 위임은 명시적으로 변경되거나 해제될 때까지 지속됩니다. 이 예시는 일괄 결제를 보여주지만, 동일한 패턴이 컨트랙트 호출의 모든 조합에 적용됩니다.
사용 사례: 지출 한도
위임 컨트랙트는 계정 마이그레이션 없이 EOA에 트랜잭션당 또는 일일 상한을 적용할 수 있습니다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/// @title SpendingLimitExecutor
/// @notice Delegate contract that enforces daily spending caps
contract SpendingLimitExecutor {
mapping(address => uint256) public dailyLimit;
mapping(address => uint256) public spentToday;
mapping(address => uint256) public lastResetDay;
function setDailyLimit(uint256 limit) external {
dailyLimit[msg.sender] = limit;
}
function executeWithLimit(
address target,
uint256 value,
bytes calldata data
) external payable {
uint256 today = block.timestamp / 1 days;
if (today > lastResetDay[msg.sender]) {
spentToday[msg.sender] = 0;
lastResetDay[msg.sender] = today;
}
spentToday[msg.sender] += value;
require(
spentToday[msg.sender] <= dailyLimit[msg.sender],
"daily limit exceeded"
);
(bool success,) = target.call{value: value}(data);
require(success, "call failed");
}
}사용 사례: 세션 키
세션 키를 사용하면 dApp이 범위가 지정된 권한(시간 창과 허용된 대상 컨트랙트 집합) 내에서 EOA를 대신하여 트랜잭션을 실행할 수 있습니다. 이는 빈번한 온체인 상호작용이 그렇지 않으면 반복적인 지갑 승인을 요구하게 되는 dApp에 유용합니다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/// @title SessionKeyExecutor
/// @notice Delegate contract that grants scoped, time-limited access to a session key
contract SessionKeyExecutor {
struct Session {
address key;
uint256 validUntil;
uint256 spendingLimit;
uint256 spent;
}
mapping(address => Session) public sessions;
mapping(address => mapping(address => bool)) public allowedTargets;
/// @notice Register a session key with scoped permissions
function startSession(
address key,
uint256 validUntil,
uint256 spendingLimit,
address[] calldata targets
) external {
sessions[msg.sender] = Session({
key: key,
validUntil: validUntil,
spendingLimit: spendingLimit,
spent: 0
});
for (uint256 i = 0; i < targets.length; i++) {
allowedTargets[msg.sender][targets[i]] = true;
}
}
/// @notice Execute a call using the session key
function executeAsSessionKey(
address owner,
address target,
uint256 value,
bytes calldata data
) external {
Session storage session = sessions[owner];
require(msg.sender == session.key, "not session key");
require(block.timestamp <= session.validUntil, "session expired");
require(allowedTargets[owner][target], "target not allowed");
uint256 beforeBalance = owner.balance;
(bool success,) = target.call{value: value}(data);
require(success, "call failed");
session.spent += owner.balance - beforeBalance;
require(session.spent <= session.spendingLimit, "budget exceeded");
}
/// @notice Revoke the active session
function revokeSession() external {
delete sessions[msg.sender];
}
}중요 고려 사항
- 지속적인 위임: 위임은 EOA가 명시적으로 변경하거나 해제할 때까지 지속됩니다. 단일 트랜잭션에 국한되지 않습니다.
- 가스 비용: EIP-7702 트랜잭션은 권한 부여 처리로 인해 기본 가스가 약간 더 높지만, 위임이 여러 호출을 일괄 처리할 때 상쇄됩니다.
- 감사받은 위임 사용: 악의적인 위임 컨트랙트는 EOA의 자산을 탈취할 수 있습니다. 감사받은 컨트랙트에만 위임하세요.
핵심 요점
- EIP-7702를 사용하면 EOA가 새로운 계정 유형으로 마이그레이션하지 않고도 스마트 컨트랙트 로직을 실행할 수 있습니다.
- Stable에서 EIP-7702는 기존 EOA에서 일괄 결제, 지출 한도, 범위가 지정된 세션 키를 가능하게 합니다.
- 위임은 명시적으로 변경될 때까지 지속됩니다. 항상 감사받은 위임 컨트랙트를 사용하세요.
다음 추천
- 구독 및 수금 — SubscriptionManager를 사용하여 반복 구독 결제에 EIP-7702를 적용합니다.
- EIP-7702 개념 — 배포하기 전에 위임 모델을 이해하세요.
- EIP-7702 레퍼런스 —
0x04트랜잭션 형식과 권한 부여 필드를 찾아보세요.

