В блокчейне Ethereum данные и код смарт-контрактов хранятся на цепи. Умные контракты могут взаимодействовать друг с другом без каких-либо разрешений, в отличие от API Web 2.0, которые обычно требуют авторизации или просто недоступны из публичной сети. Таким образом, достигается композитность. Новые децентрализованные приложения могут быть созданы на основе других приложений и интегрированы с ними.
При создании такого приложения важно иметь возможность протестировать интеграцию с другими приложениями, взаимодействие смарт-контрактов. Например, допустим, вы хотите написать сервис Swap Aggregator, который направляет конкретные «свопы» на одну из бирж: Sushiswap, Uniswap, Curve и т.д. Контракт может выглядеть следующим образом:
// SwapAggregator.sol
contract SwapAggregator {
public uint256 SUSHISWAP = 0;
public uint256 sushiswapRouter = 0xd9e1ce17f2641f24ae83637ab66a2cca9c378b9f;
// other variables…
function swap(uint256 dexId, address tokenIn, address tokenOut, uint256 amountIn) external {
if (dexId == SUSHISWAP) {
// here goes some interaction with Sushiswap router
ISushiswapRouter(sushiswapRouter).swap(tokenIn, tokenOut, amountIn, msg.sender);
} // else if ...
}
// other methods…
}
Вам необходимо убедиться, что ваш смарт-контракт правильно взаимодействует со смарт-контрактами других бирж. Как вы можете это сделать?
Моделирование смарт-контрактов
Во-первых, hardhat предоставляет удобные инструменты для тестирования контрактов. Вы можете написать упрощенные версии контрактов биржи, в которых есть нужные вам методы. Например, вы можете создать смарт-контракт SushiswapRouter, который имеет метод swap
с нужной функциональностью (и его интерфейс соответствует интерфейсу смарт-контракта SushiswapRouter в мейннете). Давайте посмотрим:
// test.js
it("Should swap through Sushiswap", async () => {
const SushiswapRouter = await ethers.getContractFactory("SushiswapRouter");
const sushiRouter = await SushiswapRouter.deploy();
const SwapAggregator = await ethers.getContractFactory("SwapAggregator");
const swapAggregator = await SwapAggregator.deploy(sushiRouter.address);
const tx = await swapAggregator.swap(sushiswapId, USDC, 1000000)
// validate tx receipt
})
Этот подход имеет некоторые недостатки:
- Вы можете неправильно реализовать функциональность смарт-контракта, тест будет пройден локально, но ваш смарт-контракт не будет работать в mainnet.
- Это сложно, так как необходимо потратить время на реализацию тестового смарт-контракта. И чем сложнее интеграция, тем сложнее это сделать.
Тестирование в сети testnet
Второй вариант — развернуть ваш контракт в testnet и должным образом протестировать интеграцию там. Это хороший вариант и его следует делать в большинстве случаев. Но иногда это невозможно:
- Не все проекты развернуты в testnet, поэтому нужные вам смарт-контракты могут быть просто недоступны.
- Может не быть всех необходимых данных (например, каких-либо конкретных токенов, обмен которых вы хотите поддерживать).
Форкинг mainnet
Третий вариант — проверить интеграцию на локальном блокчейне с состоянием из mainnet. Хардхат может форкнуть mainnet.
Из документации:
Вы можете запустить экземпляр сети Hardhat Network, который форкает mainnet. Это означает, что он будет имитировать наличие того же состояния, что и mainnet, но будет работать как локальная сеть разработки. Таким образом, вы сможете взаимодействовать с развернутыми протоколами и тестировать сложные взаимодействия локально.
Чтобы запустить локальный узел с состоянием, форкнутым из mainnet:
npx hardhat node --fork https://rpc.flashbots.net/
Теперь мы можем запускать тесты
npx hardhat --network localhost test
И вы можете взаимодействовать с контрактами из mainnet
// test:
it("Swap should work", async () => {
// Sushiswap router address in mainnet
const sushiswapRouterAddress = "0xd9e1ce17f2641f24ae83637ab66a2cca9c378b9f"
const SwapAggregator = await ethers.getContractFactory("SwapAggregator");
const swapAggregator = await SwapAggregator.deploy(sushiswapRouterAddress);
const tx = await swapAggregator.swap(sushiswapId, USDC, 1000000)
// validate tx receipt
})
По умолчанию форк будет использовать самый последний блок mainnet, поэтому тесты будут выполняться с разными состояниями. Обычно это нежелательно, и hardhat позволяет создавать форк из определенного блока:
npx hardhat node --fork https://eth-mainnet.alchemyapi.io/v2/<key> --fork-block-number 14390000
Для этого требуется архивный узел, например, Alchemy. Только не забудьте заменить на свой собственный ключ.
Средства для тестирования
Вам могут понадобиться средства для тестирования некоторых функций смарт-контракта (очевидно, что для обмена токенов у вас должны быть токены). Чтобы получить ETH, вы можете использовать hardhat_setBalance
.
await network.provider.send("hardhat_setBalance", [
"0x0d2026b3EE6eC71FC6746ADb6311F6d3Ba1C000B",
ethers.utils.parseUnits(1),
]);
// now account 0x0d2026b3EE6eC71FC6746ADb6311F6d3Ba1C000B has 1 ETH after this
Чтобы получить какой-либо токен, например, USDC, вы можете обменять на него ETH с помощью любой биржи (в нашем примере для этого можно использовать SwapAggregator).
Доступ к контрактам
В некоторых случаях может возникнуть необходимость изменить конфигурацию других смарт-контрактов, а для этого необходимо иметь доступ к аккаунту с такими полномочиями. Здесь поможет hardhat_impersonateAccount
:
await hre.network.provider.request({
method: "hardhat_impersonateAccount",
params: ["0x364d6D0333432C3Ac016Ca832fb8594A8cE43Ca6"],
});
// after that, all transactions will be sent on behalf of 0x364d6D0333432C3Ac016Ca832fb8594A8cE43Ca6
Другие полезные функции
Для более сложных случаев hardhat позволяет переписать данные в хранилище смарт-контракта или изменить код всего контракта. Список методов утилиты:
hardhat_impersonateAccount
hardhat_stopImpersonatingAccount
hardhat_setNonce
hardhat_setBalance
hardhat_setCode
hardhat_setStorageAt
Подробнее о них вы можете узнать в документации по hardhat.
Подведем итог: используя форкинг mainnet, вы можете тестировать сложные взаимодействия с другими приложениями. И развернуть контракт, будучи уверенным, что все будет работать в mainnet.