Сегодня начнем разбирать тему тестирования контрактов с EIP-7702 в Foundry. И для начала нам потребуется некоторая подготовительная работа.
Создадим проект и папку, установим зависимости и настроим remapping:
forge init eip-7702-project
cd eip-7702-project
forge install foundry-rs/forge-std
forge install OpenZeppelin/openzeppelin-contracts
forge remappings > remappings.txt
Теперь нам нужно обновить версию EVM для корректных тестов. Измените файл foundry.toml, чтобы обеспечить совместимость с EIP-7702, установив хардфорк Prague. Добавьте следующую строку в раздел [profile.default]:
evm_version = «prague»
Это необходимо, поскольку EIP-7702 доступен только с обновления Prague и далее.
Очистите преднастроенные контракты, тесты и скрипты. В папку src добавьте файл BatchCallAndSponsor, в папку test - файл BatchCallAndSponsor.t.sol, в папку script - файл BatchCallAndSponsor.s.sol из приложения к посту.
Контракт BatchCallAndSponsor - это простой контракт логики, который поддерживает пакетные транзакции и спонсорский газ.
P.S. Полный вариант реализации смотрите в файле BatchCallAndSponsor.sol. А в приведенных ниже контрактах дается краткий обзор возможностей.
Функция execute в контракте принимает массив структур Call, каждая из которых представляет отдельный вызов и указывает целевой адрес, значение (в Ether) и данные calldata.
struct Call {
address to;
uint256 value;
bytes data;
}
function execute(Call[] calldata calls) external payable {
require(msg.sender == address(this), "Invalid authority");
_executeBatch(calls);
}
function _executeBatch(Call[] calldata calls) internal {
uint256 currentNonce = nonce;
nonce++;
for (uint256 i = 0; i < calls.length; i++) {
_executeCall(calls[i]);
}
emit BatchExecuted(currentNonce, calls);
}
function _executeCall(Call calldata callItem) internal {
(bool success,) = callItem.to.call{value: callItem.value}(callItem.data);
require(success, "Call reverted");
emit CallExecuted(msg.sender, callItem.to, callItem.value, callItem.data);
}Контракт проверяет подпись с помощью библиотеки OpenZeppelin ECDSA и MessageHashUtils. Подписанное сообщение содержит адрес вызывающей стороны, целевой контракт и вызовы.
bytes32 digest = keccak256(abi.encodePacked(nonce, encodedCalls));
require(ECDSA.recover(digest, signature) == msg.sender, "Invalid signature");
В контракте поддерживается либо прямое, либо спонсируемое исполнение.
1. Прямое исполнение: Вызывающая сторона сама выполняет вызовы.
2. Спонсируемое исполнение: Спонсор выполняет вызовы от имени вызывающего абонента после проверки подписи, сделанной EOA.
function execute(Call[] calldata calls) external payable {
// The caller executes the calls directly
}
function execute(Call[] calldata calls, bytes calldata signature) external payable {
// A sponsor executes the calls on behalf of the caller
}Для предотвращения атак повторного воспроизведения в контракте используется nonce. После каждого успешного выполнения транзакции значение nonce увеличивается. Если бы nonce не был реализован, злоумышленник мог бы воспроизвести одну и ту же транзакцию несколько раз.
function _executeBatch(Call[] calldata calls) internal {
uint256 currentNonce = nonce;
nonce++; // Increment nonce to protect against replay attacks
for (uint256 i = 0; i < calls.length; i++) {
_executeCall(calls[i]);
}
emit BatchExecuted(currentNonce, calls);
}Далее поговорим о самих тестах.
#eip7702