Разработка системы gasless cross-chain транзакций
Gasless транзакции решают один из главных барьеров Web3: пользователю нужны нативные токены на каждой цепи чтобы платить газ. Хочешь использовать Arbitrum — нужен ETH на Arbitrum. Хочешь перейти на Base — нужен ETH на Base. Это плохой UX. Gasless cross-chain идёт дальше: пользователь не только не думает о газе, но и не думает о том, на какой цепи он находится.
Компоненты системы
Gasless cross-chain — не один продукт, а стек из нескольких слоёв:
Layer 1: Meta-transactions / ERC-4337. Кто платит газ на исходной цепи.
Layer 2: Paymaster. Спонсор, который покрывает стоимость газа (или принимает оплату в ERC-20).
Layer 3: Cross-chain relayer. Кто передаёт сообщение и платит газ на целевой цепи.
Layer 4: Solver/intent executor. Кто находит оптимальный маршрут исполнения.
ERC-4337 как фундамент
Без ERC-4337 каждая gasless схема — кастомный костыль. С ERC-4337 — стандартизированный framework:
// UserOperation — стандартная единица gasless транзакции
interface UserOperation {
sender: string; // smart account адрес
nonce: bigint;
initCode: string; // для деплоя аккаунта если не существует
callData: string; // что выполнить
callGasLimit: bigint;
verificationGasLimit: bigint;
preVerificationGas: bigint;
maxFeePerGas: bigint;
maxPriorityFeePerGas: bigint;
paymasterAndData: string; // paymaster адрес + данные
signature: string;
}
Bundler собирает UserOperations, упаковывает в транзакцию и отправляет on-chain. Он платит газ и возмещает его из paymaster или из аккаунта пользователя.
Paymaster реализация
Paymaster — смарт-контракт, который решает «кто платит газ»:
contract ERC20Paymaster is BasePaymaster {
address public acceptedToken; // например USDC
AggregatorV3Interface public priceFeed; // Chainlink price feed
function _validatePaymasterUserOp(
UserOperation calldata userOp,
bytes32 userOpHash,
uint256 maxCost // максимальный газ в ETH
) internal override returns (bytes memory context, uint256 validationData) {
// Рассчитываем сколько USDC нужно за maxCost газа
uint256 tokenAmount = _calculateTokenAmount(maxCost);
// Добавляем 10% буфер на случай роста gas price
tokenAmount = tokenAmount * 110 / 100;
// Проверяем что пользователь одобрил достаточно токенов
require(
IERC20(acceptedToken).allowance(userOp.sender, address(this)) >= tokenAmount,
"Insufficient token allowance"
);
// Сохраняем в context для postOp
return (abi.encode(userOp.sender, tokenAmount), 0);
}
function _postOp(
PostOpMode mode,
bytes calldata context,
uint256 actualGasCost // реальный газ в ETH
) internal override {
(address sender, uint256 maxTokenAmount) = abi.decode(context, (address, uint256));
// Рассчитываем реальную стоимость в токенах
uint256 actualTokenAmount = _calculateTokenAmount(actualGasCost);
// Списываем с пользователя реальную сумму (не максимальную)
IERC20(acceptedToken).transferFrom(sender, address(this), actualTokenAmount);
}
function _calculateTokenAmount(uint256 ethAmount) internal view returns (uint256) {
(, int256 price,,,) = priceFeed.latestRoundData(); // ETH/USDC price
return (ethAmount * uint256(price)) / 1e18;
}
}
Cross-chain gas relay
Gasless на исходной цепи — это первая половина. Вторая: кто платит газ на целевой цепи для исполнения cross-chain сообщения?
Несколько подходов:
Axelar Gas Service
При отправке сообщения через Axelar — оплачиваем газ для целевой цепи заранее, в нативном токене исходной цепи:
function sendGaslessMessage(
string calldata destChain,
string calldata destContract,
bytes calldata payload
) external payable {
// msg.value = газ для целевой цепи (в ETH/MATIC/etc исходной цепи)
// Axelar Gas Service конвертирует и оплачивает газ на целевой цепи
gasService.payNativeGasForContractCall{value: msg.value}(
address(this), destChain, destContract, payload, msg.sender
);
gateway.callContract(destChain, destContract, payload);
}
Пользователь платит один раз на исходной цепи, Axelar берёт на себя оплату газа на всех промежуточных и целевых цепях.
LayerZero с нативным gas drop
LayerZero позволяет «дропнуть» нативный газ на адрес получателя на целевой цепи:
function sendWithGasDrop(
uint16 dstChainId,
bytes calldata payload,
address payable refundAddress,
address zroPaymentAddress,
uint256 dstNativeAmount, // сколько нативного токена получит адрес на dst
address dstNativeAddress // кто получит нативные токены
) external payable {
bytes memory adapterParams = abi.encodePacked(
uint16(2), // version 2 = с airdrop
uint256(200000), // gas limit на dst
dstNativeAmount,
dstNativeAddress
);
lzEndpoint.send{value: msg.value}(
dstChainId,
abi.encode(destContract),
payload,
refundAddress,
zroPaymentAddress,
adapterParams
);
}
Это позволяет «снабдить» новый адрес на целевой цепи небольшим количеством ETH для первых транзакций.
Relayer сеть с собственными нодами
Для полностью кастомной системы — собственная сеть relayer нод, каждая из которых держит баланс нативных токенов на всех поддерживаемых цепях:
class CrossChainRelayer {
// Балансы на всех цепях
private chainWallets: Map<number, Wallet> = new Map();
async relayMessage(
sourceChain: number,
destChain: number,
contractAddress: string,
calldata: string,
userSignature: string
): Promise<string> {
// Верифицируем подпись пользователя
const isValid = await this.verifyUserSignature(userSignature, calldata);
if (!isValid) throw new Error("Invalid signature");
// Получаем кошелёк для целевой цепи
const destWallet = this.chainWallets.get(destChain);
if (!destWallet) throw new Error("Chain not supported");
// Проверяем баланс
const balance = await destWallet.provider!.getBalance(destWallet.address);
const gasEstimate = await destWallet.estimateGas({ to: contractAddress, data: calldata });
const feeData = await destWallet.provider!.getFeeData();
const gasCost = gasEstimate * feeData.maxFeePerGas!;
if (balance < gasCost * 2n) {
// Нужна пополнение баланса relayer
await this.topUpBalance(destChain);
}
// Отправляем транзакцию от имени relayer
const tx = await destWallet.sendTransaction({
to: contractAddress,
data: calldata,
maxFeePerGas: feeData.maxFeePerGas,
maxPriorityFeePerGas: feeData.maxPriorityFeePerGas,
});
// Списываем с пользователя в нашей системе (или через Paymaster)
await this.chargeUser(userSignature, gasCost);
return tx.hash;
}
}
Intent-based gasless архитектура
Самый продвинутый вариант: пользователь подписывает intent, solver находит оптимальный маршрут и исполняет его, оплачивая всё с прибыли от арбитража:
interface GaslessIntent {
user: string;
sourceChain: number;
destChain: number;
action: string; // "swap", "stake", "transfer"
inputToken: string;
inputAmount: string;
minOutputAmount: string; // solver должен обеспечить minimum output
deadline: number;
signature: string;
}
// Solver берёт intent и исполняет его
// Прибыль: разница между реальным output и minOutputAmount
// Из этой прибыли оплачивается газ на обеих цепях
class IntentSolver {
async solve(intent: GaslessIntent): Promise<void> {
// Расчёт профитабельности
const route = await this.findOptimalRoute(intent);
const expectedOutput = await this.simulate(route, intent);
const gasCosts = await this.estimateTotalGasCosts(route);
const profit = expectedOutput - BigInt(intent.minOutputAmount) - gasCosts;
if (profit < this.minProfitThreshold) {
return; // Нерентабельно
}
// Исполняем
await this.executeRoute(route, intent);
}
}
Permit2 для gasless approvals
Традиционный approve требует отдельную транзакцию (gas). С Permit2 (Uniswap):
// Пользователь подписывает разрешение off-chain (zero gas)
const permit = {
permitted: { token: USDC_ADDRESS, amount: parseUnits("100", 6) },
spender: RELAYER_ADDRESS,
nonce: await getPermitNonce(userAddress),
deadline: Math.floor(Date.now() / 1000) + 3600,
};
const signature = await signer._signTypedData(
{ name: "Permit2", chainId: 1, verifyingContract: PERMIT2_ADDRESS },
PERMIT2_TYPES,
permit
);
// Relayer использует подпись для transferFrom без отдельного approve
await permit2Contract.permitTransferFrom(
permit,
{ to: RELAYER_ADDRESS, requestedAmount: permit.permitted.amount },
userAddress,
signature
);
Экономика системы
Кто платит за газ в итоге? Варианты:
| Модель | Кто платит | Когда подходит |
|---|---|---|
| Sponsored (freemium) | Приложение | Onboarding, gaming, loyalty |
| ERC-20 paymaster | Пользователь в stablecoin | DeFi, trading |
| Solver extracts surplus | Solver из арбитража | Intent-based протоколы |
| Fee token swap | Система конвертирует fee token | Общий случай |
Стек
Smart contracts: Solidity + ERC-4337 + Permit2 + Foundry Bundler: Pimlico, StackUp, Alchemy (hosted) или Alto (self-hosted) Paymaster: кастомный ERC20Paymaster + Pimlico sponsored Cross-chain: Axelar Gas Service или LayerZero с adapterParams Relayer: Node.js + TypeScript + viem Frontend: wagmi v2 + permissionless.js
Сроки
- Gasless на одной цепи (ERC-4337 + ERC-20 Paymaster): 3-4 недели
- Cross-chain gas relay (Axelar/LayerZero интеграция): +3-4 недели
- Intent solver (profitable solving + routing): +4-6 недель
- Production + мониторинг + security audit: +4-6 недель
- Итого полная система: 3-4 месяца







