Разработка системы Permit2 (токен-апрувалы)
Классический ERC-20 требует двух транзакций для любого взаимодействия с DeFi: approve() и затем transferFrom(). Это UX-проблема и проблема безопасности одновременно — бесконечные approve на сторонние протоколы стали причиной потери миллионов долларов при взломах (одобрили протокол, его взломали, атакующий дренировал все одобренные токены). EIP-2612 добавил permit() для подписанных апрувалов без транзакций, но поддерживают его не все токены. Permit2 от Uniswap решает оба вопроса на уровне единого контракта-хаба.
Как работает Permit2
Идея проста: пользователь один раз делает approve(Permit2Address, type(uint256).max) для каждого токена. После этого Permit2 контракт управляет разрешениями от его имени — но уже через подписанные off-chain сообщения, без дополнительных транзакций.
Два режима работы:
AllowanceTransfer — аналог стандартного ERC-20 approve, но через Permit2. Разрешение имеет expiration. Протокол запрашивает у пользователя подпись PermitSingle или PermitBatch, после чего может вызывать transferFrom через Permit2.
SignatureTransfer — одноразовый перевод по подписи. Нет постоянного разрешения — только конкретная транзакция, подписанная off-chain. Атомарна: если транзакция reverted — нonce отмечается как использованный, но перевод не происходит.
Интеграция в смарт-контракт
import {IPermit2} from "permit2/src/interfaces/IPermit2.sol";
import {ISignatureTransfer} from "permit2/src/interfaces/ISignatureTransfer.sol";
contract MyProtocol {
IPermit2 public immutable permit2;
constructor(address _permit2) {
permit2 = IPermit2(_permit2);
}
// Принимаем токены через SignatureTransfer (разовый перевод)
function deposit(
uint256 amount,
ISignatureTransfer.PermitTransferFrom memory permit,
bytes calldata signature
) external {
permit2.permitTransferFrom(
permit,
ISignatureTransfer.SignatureTransferDetails({
to: address(this),
requestedAmount: amount
}),
msg.sender,
signature
);
// amount токенов уже у нас, продолжаем логику
_processDeposit(msg.sender, amount);
}
}
На стороне фронтенда (wagmi/viem):
import { signTypedData } from "wagmi/actions";
const permitData = {
permitted: {
token: tokenAddress,
amount: parseEther("100"),
},
nonce: await getPermit2Nonce(userAddress),
deadline: BigInt(Math.floor(Date.now() / 1000) + 3600), // 1 час
};
const signature = await signTypedData({
domain: {
name: "Permit2",
chainId: 1,
verifyingContract: PERMIT2_ADDRESS,
},
types: PERMIT_TRANSFER_FROM_TYPES,
primaryType: "PermitTransferFrom",
message: permitData,
});
// Отправляем в контракт depositData + signature
Управление nonce в SignatureTransfer
В отличие от стандартного EIP-2612, где nonce линейный (каждая подпись = следующий nonce), Permit2 использует bitmap-based nonce. Nonce — это 256-битное число, где wordPosition = nonce >> 8 и bitPosition = nonce & 0xFF. Это позволяет инвалидировать конкретные разрешения без необходимости использовать все предыдущие nonce по порядку.
Пользователь может отозвать конкретный pending nonce через:
permit2.invalidateUnorderedNonces(wordPosition, mask);
Это критически важно для UX: если пользователь подписал разрешение с длинным deadline, он может отозвать его без новой транзакции approve.
AllowanceTransfer: постоянные разрешения с expiration
// Проверяем и используем существующее AllowanceTransfer разрешение
function depositWithAllowance(address token, uint160 amount) external {
permit2.transferFrom(msg.sender, address(this), amount, token);
// Permit2 проверит: есть ли разрешение, не истёк ли срок, достаточно ли суммы
}
Преимущество перед стандартным transferFrom: разрешение автоматически истекает по expiration. Пользователь видит в интерфейсе Permit2: «вы одобрили X токенов до Y даты». Это прозрачнее бесконечного ERC-20 approve.
Типичные ошибки интеграции
Не проверять requestedAmount против фактически переданного. Если контракт принимает любой amount из permit, атакующий может передать permit на большую сумму, но вывести меньше — или наоборот, если логика некорректна.
Hardcoded Permit2 address. Permit2 задеплоен по одному адресу (0x000000000022D473030F116dDEE9F6B43aC78BA3) через CREATE2 на всех EVM-сетях. Используем как константу, не как constructor-параметр в production.
Не обрабатывать revert от permit2. Если signature недействительна или nonce уже использован — permit2 reverted. Убедиться, что контракт корректно пропагирует этот revert, а не маскирует его.
Процесс работы
Анализ. Определяем, какой режим нужен: AllowanceTransfer для протоколов с постоянным доступом к средствам, SignatureTransfer для атомарных операций. Чаще используем комбинацию.
Разработка. Интеграция в смарт-контракт + типы для typed data signing на фронтенде. Foundry-тесты с PermitSignature хелпером из permit2 репозитория.
Тестирование. Fork-тесты на mainnet с реальным Permit2 контрактом. Проверяем сценарии истёкшего deadline, использованного nonce, неверной подписи.
Ориентиры по срокам
Интеграция Permit2 в существующий контракт + фронтенд: 2-3 дня. Включает оба режима (AllowanceTransfer и SignatureTransfer), тесты, и UI для просмотра/отзыва разрешений.







