在 Brownie、Hardhat 或 Truffle 测试套件中部署 Uniswap v2 / Sushiswap 或类似产品
Deploying Uniswap v2 / Sushiswap or similar in Brownie, Hardhat or Truffle test suite
我正在编写一个自动化测试套件,需要针对 Uniswap v2 样式的自动市场标记测试功能:进行掉期并使用不同的订单路由。因此,需要部署路由器。
是否有关于如何在 Brownie 中部署可测试的 Uniswap v2 样式交易所的现有示例?因为Brownie是少数智能合约开发者,有没有Truffle或者Hardhat的例子?
我也在探索使用主网分叉的选项,但我不确定此操作是否太昂贵(太慢)而无法用于单元测试。
使用本地测试网可以让您在测试期间非常精确地控制区块链的状态。但是,它需要您手动部署您需要的每个合约。
主网的一个分支将使您不必部署已部署在主网上的每个合约。但是,您将牺牲对环境的控制,并且需要连接到节点。
我已经在测试网上部署了几次 Uniswap 2V。为此,您需要以下合约的字节码和 ABI:UniswapV2Factory、UniswapV2Pair、UniswapV2Router02(我想您需要路由器的第二个版本)。 Uniswap 文档很好地解释了如何从 NPM 下载它们。为了让路由器正常工作,您还需要部署 WETH 合约。我建议部署这个 github page.
在 运行ning 这段代码之前,只需确保您的链是 运行ning。对于安全帽 运行 以下命令:
npx hardhat node
首先将您的签名者连接到您的开发链:
var provider = new ethers.providers.WebSocketProvider("ws://localhost:8545");
var signer = provider.getSigner();
使用 ethers.js 库,首先部署工厂:
const compiledUniswapFactory = require("@uniswap/v2-core/build/UniswapV2Factory.json");
var uniswapFactory = await new ethers.ContractFactory(compiledUniswapFactory.interface,compiledUniswapFactory.bytecode,signer).deploy(await signer.getAddress());
那么WETH合约:
const compiledWETH = require("canonical-weth/build/conrtacts/WETH.json";
var WETH = await new ethers.ContractFactory(WETH.interface,WETH.bytecode,signer).deploy();
您现在可以部署路由器了。
const compiledUniswapRouter = require("@uniswap/v2-periphery/build/UniswapV2Router02");
var router = await new ethers.ContractFactory(compiledUniswapRouter.abi,compiledUniswapRouter.bytecode,signer).deploy(uniswapFactory.address,WETH.address);
您还需要部署所需的 ERC20 代币(这是我编写的代币示例):
const compiledERC20 = require("../../../Ethereum/Ethereum/sources/ERC20.sol/Token.json");
var erc20Factory = new ethers.ContractFactory(compiledERC20.abi,compiledERC20.bytecode,signer);
var erc20_0 = await erc20Factory.deploy("1000000", "Token 0", "5", "T0");
var erc20_1 = await erc20Factory.deploy("1000000", "Token 1", "5", "T1");
部署函数的参数将取决于您要部署的令牌的构造函数。
您还需要使用 Uniswap 工厂的 createPair 方法创建交易对。
uniswapFactory.createPair(erc20_0.address,erc20_1.address);
请记住,在交易对中,代币将由合约任意排序。 ERC20_0 可能不是两者中的第一个。
之后只需等待所有交易完成,您就可以开始测试了。
Xavier Hamel 的回答为我指明了正确的方向。
不幸的是,如果不编辑源代码,就无法重新编译和重新部署 Uniswap v2 及其克隆。这是因为
- 路由器依赖于UniswapV2Library
- UniswapV2Library 在 pairFor() 函数
中硬编码了 UniswapV2Factory 的初始代码哈希
- 任何重新编译都可能导致不同的
UniswapV2Factory
,从而导致不同的散列
- 路由器将无法路由任何交易,因为
pairFor
失败,因为它生成无效的配对合约地址
This was a pesky problem. I ended up building the whole library and tooling to solve it: Please meet Smart contacts for testing。它基于 Sushiswap v2 回购协议,因为他们的合约得到了最好的维护。
为了快速简便地设置,我会使用 Hardhat 的主网分支。
就像@Xavier 所说的那样,使用主网分支意味着您不必部署要与之交互的每个合约。如果您想针对多个交易所测试您的合约,这将是最简单的方法。但是,它确实需要连接到节点,因此您的单元测试会 运行 变慢。
举个例子,假设我想测试以下合约,它使用 swapExactETHForTokens
方法在 Uniswap 上将 ETH 换成 ERC20 代币。
pragma solidity ^0.6.6;
interface IUniswap {
function swapExactETHForTokens(
uint amountOutMin,
address[] calldata path,
address to,
uint deadline)
external
payable
returns (uint[] memory amounts);
function WETH() external pure returns (address);
}
contract UniswapTradeExample {
IUniswap uniswap;
// Pass in address of UniswapV2Router02
constructor(address _uniswap) public {
uniswap = IUniswap(_uniswap);
}
function swapExactETHForTokens(uint amountOutMin, address token) external payable {
address[] memory path = new address[](2);
path[0] = uniswap.WETH();
path[1] = token;
uniswap.swapExactETHForTokens{value: msg.value}(
amountOutMin,
path,
msg.sender,
now
);
}
}
根据 Hardhat 文档,要做的第一件事是使用 Alchemy 建立到存档节点的连接,可以免费使用。
一旦您拥有节点的 url,将其作为网络添加到您的 hardhat.config.js 文件中:
networks: {
hardhat: {
forking: {
url: "https://eth-mainnet.alchemyapi.io/v2/<key>",
blockNumber: 14189520
}
}
}
如果设置blockNumber
,Hardhat每次都会分叉出这个区块。如果您正在使用测试套件并希望您的测试具有确定性,则推荐这样做。
最后,这里是测试class,它测试了上面合约中的swapExactETHForTokens
。例如,我将 1 ETH 换成 DAI。
const { assert } = require("chai");
const { ethers } = require("hardhat");
const ERC20ABI = require('./ERC20.json');
const UNISWAPV2ROUTER02_ADDRESS = "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D";
const DAI_ADDRESS = "0x6b175474e89094c44da98b954eedeac495271d0f";
describe("UniswapTradeExample", function () {
it("Swap ETH for DAI", async function () {
const provider = ethers.provider;
const [owner, addr1] = await ethers.getSigners();
const DAI = new ethers.Contract(DAI_ADDRESS, ERC20ABI, provider);
// Assert addr1 has 1000 ETH to start
addr1Balance = await provider.getBalance(addr1.address);
expectedBalance = ethers.BigNumber.from("10000000000000000000000");
assert(addr1Balance.eq(expectedBalance));
// Assert addr1 DAI balance is 0
addr1Dai = await DAI.balanceOf(addr1.address);
assert(addr1Dai.isZero());
// Deploy UniswapTradeExample
const uniswapTradeExample =
await ethers.getContractFactory("UniswapTradeExample")
.then(contract => contract.deploy(UNISWAPV2ROUTER02_ADDRESS));
await uniswapTradeExample.deployed();
// Swap 1 ETH for DAI
await uniswapTradeExample.connect(addr1).swapExactETHForTokens(
0,
DAI_ADDRESS,
{ value: ethers.utils.parseEther("1") }
);
// Assert addr1Balance contains one less ETH
expectedBalance = addr1Balance.sub(ethers.utils.parseEther("1"));
addr1Balance = await provider.getBalance(addr1.address);
assert(addr1Balance.lt(expectedBalance));
// Assert DAI balance increased
addr1Dai = await DAI.balanceOf(addr1.address);
assert(addr1Dai.gt(ethers.BigNumber.from("0")));
});
});
注意顶部的 const ERC20ABI = require('./ERC20.json');
,它导入获取 DAI 合约所需的 ERC20ABI 并使用它的 balanceOf()
方法。
就是这样。 运行 npx hardhat test
应该表明这个测试通过了。
我正在编写一个自动化测试套件,需要针对 Uniswap v2 样式的自动市场标记测试功能:进行掉期并使用不同的订单路由。因此,需要部署路由器。
是否有关于如何在 Brownie 中部署可测试的 Uniswap v2 样式交易所的现有示例?因为Brownie是少数智能合约开发者,有没有Truffle或者Hardhat的例子?
我也在探索使用主网分叉的选项,但我不确定此操作是否太昂贵(太慢)而无法用于单元测试。
使用本地测试网可以让您在测试期间非常精确地控制区块链的状态。但是,它需要您手动部署您需要的每个合约。
主网的一个分支将使您不必部署已部署在主网上的每个合约。但是,您将牺牲对环境的控制,并且需要连接到节点。
我已经在测试网上部署了几次 Uniswap 2V。为此,您需要以下合约的字节码和 ABI:UniswapV2Factory、UniswapV2Pair、UniswapV2Router02(我想您需要路由器的第二个版本)。 Uniswap 文档很好地解释了如何从 NPM 下载它们。为了让路由器正常工作,您还需要部署 WETH 合约。我建议部署这个 github page.
在 运行ning 这段代码之前,只需确保您的链是 运行ning。对于安全帽 运行 以下命令:
npx hardhat node
首先将您的签名者连接到您的开发链:
var provider = new ethers.providers.WebSocketProvider("ws://localhost:8545");
var signer = provider.getSigner();
使用 ethers.js 库,首先部署工厂:
const compiledUniswapFactory = require("@uniswap/v2-core/build/UniswapV2Factory.json");
var uniswapFactory = await new ethers.ContractFactory(compiledUniswapFactory.interface,compiledUniswapFactory.bytecode,signer).deploy(await signer.getAddress());
那么WETH合约:
const compiledWETH = require("canonical-weth/build/conrtacts/WETH.json";
var WETH = await new ethers.ContractFactory(WETH.interface,WETH.bytecode,signer).deploy();
您现在可以部署路由器了。
const compiledUniswapRouter = require("@uniswap/v2-periphery/build/UniswapV2Router02");
var router = await new ethers.ContractFactory(compiledUniswapRouter.abi,compiledUniswapRouter.bytecode,signer).deploy(uniswapFactory.address,WETH.address);
您还需要部署所需的 ERC20 代币(这是我编写的代币示例):
const compiledERC20 = require("../../../Ethereum/Ethereum/sources/ERC20.sol/Token.json");
var erc20Factory = new ethers.ContractFactory(compiledERC20.abi,compiledERC20.bytecode,signer);
var erc20_0 = await erc20Factory.deploy("1000000", "Token 0", "5", "T0");
var erc20_1 = await erc20Factory.deploy("1000000", "Token 1", "5", "T1");
部署函数的参数将取决于您要部署的令牌的构造函数。
您还需要使用 Uniswap 工厂的 createPair 方法创建交易对。
uniswapFactory.createPair(erc20_0.address,erc20_1.address);
请记住,在交易对中,代币将由合约任意排序。 ERC20_0 可能不是两者中的第一个。
之后只需等待所有交易完成,您就可以开始测试了。
Xavier Hamel 的回答为我指明了正确的方向。
不幸的是,如果不编辑源代码,就无法重新编译和重新部署 Uniswap v2 及其克隆。这是因为
- 路由器依赖于UniswapV2Library
- UniswapV2Library 在 pairFor() 函数 中硬编码了 UniswapV2Factory 的初始代码哈希
- 任何重新编译都可能导致不同的
UniswapV2Factory
,从而导致不同的散列 - 路由器将无法路由任何交易,因为
pairFor
失败,因为它生成无效的配对合约地址
This was a pesky problem. I ended up building the whole library and tooling to solve it: Please meet Smart contacts for testing。它基于 Sushiswap v2 回购协议,因为他们的合约得到了最好的维护。
为了快速简便地设置,我会使用 Hardhat 的主网分支。
就像@Xavier 所说的那样,使用主网分支意味着您不必部署要与之交互的每个合约。如果您想针对多个交易所测试您的合约,这将是最简单的方法。但是,它确实需要连接到节点,因此您的单元测试会 运行 变慢。
举个例子,假设我想测试以下合约,它使用 swapExactETHForTokens
方法在 Uniswap 上将 ETH 换成 ERC20 代币。
pragma solidity ^0.6.6;
interface IUniswap {
function swapExactETHForTokens(
uint amountOutMin,
address[] calldata path,
address to,
uint deadline)
external
payable
returns (uint[] memory amounts);
function WETH() external pure returns (address);
}
contract UniswapTradeExample {
IUniswap uniswap;
// Pass in address of UniswapV2Router02
constructor(address _uniswap) public {
uniswap = IUniswap(_uniswap);
}
function swapExactETHForTokens(uint amountOutMin, address token) external payable {
address[] memory path = new address[](2);
path[0] = uniswap.WETH();
path[1] = token;
uniswap.swapExactETHForTokens{value: msg.value}(
amountOutMin,
path,
msg.sender,
now
);
}
}
根据 Hardhat 文档,要做的第一件事是使用 Alchemy 建立到存档节点的连接,可以免费使用。
一旦您拥有节点的 url,将其作为网络添加到您的 hardhat.config.js 文件中:
networks: {
hardhat: {
forking: {
url: "https://eth-mainnet.alchemyapi.io/v2/<key>",
blockNumber: 14189520
}
}
}
如果设置blockNumber
,Hardhat每次都会分叉出这个区块。如果您正在使用测试套件并希望您的测试具有确定性,则推荐这样做。
最后,这里是测试class,它测试了上面合约中的swapExactETHForTokens
。例如,我将 1 ETH 换成 DAI。
const { assert } = require("chai");
const { ethers } = require("hardhat");
const ERC20ABI = require('./ERC20.json');
const UNISWAPV2ROUTER02_ADDRESS = "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D";
const DAI_ADDRESS = "0x6b175474e89094c44da98b954eedeac495271d0f";
describe("UniswapTradeExample", function () {
it("Swap ETH for DAI", async function () {
const provider = ethers.provider;
const [owner, addr1] = await ethers.getSigners();
const DAI = new ethers.Contract(DAI_ADDRESS, ERC20ABI, provider);
// Assert addr1 has 1000 ETH to start
addr1Balance = await provider.getBalance(addr1.address);
expectedBalance = ethers.BigNumber.from("10000000000000000000000");
assert(addr1Balance.eq(expectedBalance));
// Assert addr1 DAI balance is 0
addr1Dai = await DAI.balanceOf(addr1.address);
assert(addr1Dai.isZero());
// Deploy UniswapTradeExample
const uniswapTradeExample =
await ethers.getContractFactory("UniswapTradeExample")
.then(contract => contract.deploy(UNISWAPV2ROUTER02_ADDRESS));
await uniswapTradeExample.deployed();
// Swap 1 ETH for DAI
await uniswapTradeExample.connect(addr1).swapExactETHForTokens(
0,
DAI_ADDRESS,
{ value: ethers.utils.parseEther("1") }
);
// Assert addr1Balance contains one less ETH
expectedBalance = addr1Balance.sub(ethers.utils.parseEther("1"));
addr1Balance = await provider.getBalance(addr1.address);
assert(addr1Balance.lt(expectedBalance));
// Assert DAI balance increased
addr1Dai = await DAI.balanceOf(addr1.address);
assert(addr1Dai.gt(ethers.BigNumber.from("0")));
});
});
注意顶部的 const ERC20ABI = require('./ERC20.json');
,它导入获取 DAI 合约所需的 ERC20ABI 并使用它的 balanceOf()
方法。
就是这样。 运行 npx hardhat test
应该表明这个测试通过了。