Разработка системы unified accounts
Unified accounts — пользователь имеет один идентификатор (адрес, имя, NFT) и один «логический» баланс, который автоматически используется на любой цепи. Это эволюция от «мультичейн» (много кошельков) к chain-abstracted (один кошелёк, прозрачная мультичейн). Задача нетривиальная: реализовать её правильно значит решить несколько независимых технических проблем одновременно.
Что такое unified account на техническом уровне
Наивная реализация: смарт-контракт на каждой поддерживаемой цепи с одинаковым адресом (через CREATE2), синхронизация состояния через bridge. Это работает, но создаёт сложность: состояние разрознено, синхронизация имеет задержку и стоимость.
Продвинутая реализация разделяет:
- Identity layer — кто вы (один идентификатор на всех цепях)
- Intent layer — что вы хотите сделать (абстрагировано от конкретной цепи)
- Execution layer — как это выполняется (routing, bridging, execution)
- Settlement layer — окончательный расчёт
Детерминированные адреса через CREATE2
Первый шаг — один и тот же адрес на всех EVM цепях:
// Factory контракт с одинаковым адресом на всех цепях
contract AccountFactory {
function deployAccount(
bytes32 salt,
bytes calldata initCode
) external returns (address account) {
// CREATE2: адрес зависит только от factory address + salt + initCode
// Если factory задеплоен через keyless deployment (один адрес на всех EVM),
// то и Account будет иметь одинаковый адрес
assembly {
account := create2(0, add(initCode, 32), mload(initCode), salt)
}
if (account == address(0)) revert DeploymentFailed();
}
function predictAddress(
bytes32 salt,
bytes32 initCodeHash
) external view returns (address) {
return address(uint160(uint256(keccak256(abi.encodePacked(
bytes1(0xff),
address(this),
salt,
initCodeHash
)))));
}
}
Keyless deployment (через ERC-2470 Singleton Factory или Nick's method) обеспечивает одинаковый адрес factory на всех EVM цепях. Следовательно — один и тот же salt + initCode = один адрес account на всех цепях.
Cross-chain state synchronization
Второй шаг — синхронизация данных аккаунта между цепями. Например: пользователь обновляет список owners на Ethereum, это должно отразиться на Polygon.
Паттерн: Primary chain + синхронизация через bridge:
contract UnifiedAccountPrimary {
// Primary state хранится на "home" цепи
mapping(address => bool) public owners;
uint256 public nonce;
// Синхронизация изменений на другие цепи
function addOwnerAndSync(
address newOwner,
uint64[] calldata targetChains,
address[] calldata targetAccounts
) external onlyOwner {
owners[newOwner] = true;
// Отправляем update через bridge (CCIP, Axelar, LayerZero)
for (uint i = 0; i < targetChains.length; i++) {
bytes memory payload = abi.encode(
"ADD_OWNER",
newOwner,
++nonce
);
bridge.sendMessage(targetChains[i], targetAccounts[i], payload);
}
emit OwnerAdded(newOwner);
}
}
contract UnifiedAccountReplica {
// Реплика получает обновления от Primary
uint256 public lastSyncedNonce;
function receiveSync(
bytes calldata payload,
bytes32 originMessageId
) external onlyBridge {
(string memory action, address target, uint256 nonce) =
abi.decode(payload, (string, address, uint256));
// Защита от replay: nonce должен быть следующим
require(nonce == lastSyncedNonce + 1, "Invalid nonce");
lastSyncedNonce = nonce;
if (keccak256(bytes(action)) == keccak256(bytes("ADD_OWNER"))) {
_addOwner(target);
}
}
}
Intent-based execution
Unified account принимает намерения пользователя, не конкретные транзакции. Пользователь говорит «хочу stake 100 USDC в протоколе X» — система сама решает откуда взять USDC и на какой цепи исполнить:
interface UserIntent {
action: "stake" | "swap" | "transfer" | "borrow";
targetProtocol: string;
targetChain?: number; // опционально — если не указана, система выбирает
inputToken: string;
inputAmount: string;
outputToken?: string;
minOutputAmount?: string;
deadline?: number;
}
class IntentRouter {
async resolveIntent(intent: UserIntent, userProfile: UserProfile): Promise<ExecutionPlan> {
// 1. Находим лучшую цепь для исполнения
const targetChain = intent.targetChain ||
await this.findOptimalChain(intent, userProfile);
// 2. Определяем откуда взять средства
const fundingSource = await this.findBestFundingSource(
intent.inputToken,
intent.inputAmount,
userProfile.balances,
targetChain
);
// 3. Строим execution plan
const steps: ExecutionStep[] = [];
if (fundingSource.chainId !== targetChain) {
// Нужен bridge
steps.push({
type: "bridge",
fromChain: fundingSource.chainId,
toChain: targetChain,
token: intent.inputToken,
amount: intent.inputAmount,
bridgeProtocol: await this.selectBridge(fundingSource.chainId, targetChain),
});
}
// Основное действие
steps.push({
type: intent.action,
chainId: targetChain,
protocol: intent.targetProtocol,
...
});
return {
steps,
estimatedGas: await this.estimateTotalGas(steps),
estimatedTime: this.estimateTime(steps),
};
}
}
Gasless cross-chain операции
Для полного UX unified account — пользователь не должен думать о газе. Система абстрагирует это:
// Пользователь платит в USDC, система конвертирует в нативные токены
async function executeGasless(
intent: UserIntent,
feeToken: "USDC" | "USDT" | string
): Promise<string> {
const plan = await intentRouter.resolveIntent(intent, userProfile);
// Рассчитываем общую стоимость в feeToken
const totalCostInFeeToken = await priceOracle.convertGasCost(
plan.estimatedGas,
plan.steps.map(s => s.chainId),
feeToken
);
// Получаем UserOperation со sponsored газом
const userOp = await buildSponsordUserOp(plan, feeToken, totalCostInFeeToken);
// Подписываем один раз на исходной цепи
const signedOp = await userAccount.signUserOperation(userOp);
// Executor relay выполняет все steps
return relayer.submitIntent(signedOp);
}
Account abstraction как фундамент
Unified accounts нативно строятся на ERC-4337:
- Smart account вместо EOA на каждой цепи
- UserOperations как единица intent
- Bundler как executor
- Paymaster как gas abstraction layer
ZeroDev Kernel, Safe с модулями или кастомная ERC-4337 реализация — выбор зависит от требуемой гибкости.
Проблема атомарности
Критический вопрос: что происходит если шаг 1 (bridge) прошёл успешно, а шаг 2 (stake на целевой цепи) завершился ошибкой?
Варианты:
- Optimistic execution: продолжаем, записываем pending state, retry при неудаче
- Atomic through escrow: средства находятся в escrow контракте до подтверждения финального шага
- Two-phase commit: prepare → commit/rollback
На практике большинство систем используют optimistic с retry механизмом и ручным fallback (средства остаются на целевой цепи если действие не выполнено).
Стек
| Компонент | Решение |
|---|---|
| Smart accounts | ERC-4337 (ZeroDev Kernel или Safe) |
| Cross-chain messaging | LayerZero / Axelar / CCIP |
| Gas abstraction | ERC-4337 Paymaster |
| Intent routing | Кастомный сервис + 1inch/LiFi для swaps |
| State sync | Merkle proofs + bridge |
| Frontend | wagmi v2 + viem + React |
Сроки
- Базовая unified identity (CREATE2 одинаковые адреса, базовый sync): 4-6 недель
- Intent routing + cross-chain execution: 6-8 недель
- Gas abstraction + feeToken: 3-4 недели
- Production hardening + security audit: 6-8 недель
- Итого: 4-6 месяцев







