手把手教你部署自己的uniswap交易所
之前部署是跟着崔棉大师的教程走的,但是部署完了,没法实际使用,添加流动性还是交易会报错
这里主要是做补充;
合约源代码
此处只部署routerV2
部署工厂合约和路由合约时,EVM VERSION 选择 istanbul, COMPILER CONFIGURATION 中勾选 Enable optimization ;
WETH部署时 EVM VERSION 选择 default;
工厂合约和WETH合约可以直接部署,路由合约需要修改一个Code
该步骤略, 这两个直接部署即可
weth代码中添加以下代码,便于直接获取任意数量WETH,方便测试大额交易
//直接获取WETH
function mint(uint _value)public payable{
balanceOf[msg.sender] += _value;
Deposit(msg.sender, _value);
}
initCode
重要环节,我刚开始部署的时候就是这一步不清楚导致的部署的合约,无法使用, 至于为啥会不一样,不太清楚!
获得类似这样的
得到以下结构的内容, 只需要object字段的内容; 复制
{
"linkReferences": {},
"object": "取这里的内容",
"opcodes": "-",
"sourceMap": "-"
}
打开网址 http://emn178.github.io/online-tools/keccak_256.html
将刚才得到的object字段内容粘贴,选择input type HEX
如下图
将 96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f
替换成
de683b3097cb455dd2d3ea50f1f95386fdeca75180cc01bb6b12207c44272e17 (步骤2中获取的)
// 路由中该代码
// calculates the CREATE2 address for a pair without making any external calls
function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) {
(address token0, address token1) = sortTokens(tokenA, tokenB);
pair = address(uint(keccak256(abi.encodePacked(
hex'ff',
factory,
keccak256(abi.encodePacked(token0, token1)),
hex'de683b3097cb455dd2d3ea50f1f95386fdeca75180cc01bb6b12207c44272e17' //init code hash
//hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f' //init code hash
))));
}
编译部署即可
添加流动性
ropsten
https://ropsten.etherscan.io/tx/0x90a860e95f2796b08985b02a1163eccb58efefd053e0a80ebf75cca8f7f5b8fa
rinkeby
https://rinkeby.etherscan.io/tx/0x4c82a23ec995bf404a98e39b490c5e7893945c4341495c7fe6de43b90e646aeb
新账号,所以rinkeby和ropsten两个测试网都是部署的以下地址
工厂
0x2CD020750216583CCF657a0949F0843ec1f73EFE
WETH
0x57E25a96A6dBA2cA02e9C96d08f672574c6E6B13
路由
0x9A36D38C6De905f969C172a85dD362E3Bc36B936
initCode
de683b3097cb455dd2d3ea50f1f95386fdeca75180cc01bb6b12207c44272e17
测试Token
0xa5BA457F1DfdCC3E65515E69E545292D203b0E76
另外吐槽下…
ropsten 获取测试币最简便,但是打包忒慢了
rinkeby 打包很快,不过要发推后再领, 有梯子的建议使用这个
前端代码
可以clone最好, 太慢的话就直接下载zip解压
1、自行下载好源码
2、安装好yarn
3、修改代码
修改文件: 项目目录/uniswap-interface/src/constants/index.ts 第 6 行
export const ROUTER_ADDRESS = '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D' //修改成你的路由合约地址
修改2
node_modules下面有个@uniswap/sdk/dist/constants.d.ts和sdk.esm.js
这两个文件里面修改factory地址和initCode;改完添加流动性,或者查找pair就没问题了;
如果想修改默认的weth;路径@uniswap/default-token-list/build
新版本的路径可能改了, 全局搜索下主网的weth,找到对应的替换就行
注:
前端代码可以打开IDE, 然后全局搜索替换成自己部署的信息,之后编译代码就行了!!!
//uniswap官方部署的信息
工厂
0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f
WETH
0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
路由
0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D
initCode
96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f
将以上4个信息都替换成自己部署的合约地址
工厂
0x2CD020750216583CCF657a0949F0843ec1f73EFE
WETH
0x57E25a96A6dBA2cA02e9C96d08f672574c6E6B13
路由
0x9A36D38C6De905f969C172a85dD362E3Bc36B936
initCode
de683b3097cb455dd2d3ea50f1f95386fdeca75180cc01bb6b12207c44272e17
如要替换weth,需要注意环境替换
{
mainnet:'0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
ropsten:'0xc778417E063141139Fce010982780140Aa0cD5Ab', ( "chainId": 3,)
rinkeby:'0xc778417E063141139Fce010982780140Aa0cD5Ab', ("chainId": 4)
goerli:'0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6',
kovan:'0xd0A1E359811322d97991E03f863a0C30C2cF029C'
}
替换完后,编译
$ cd uniswap-interface
$ yarn
$ yarn start
//执行 yarn start 后跳出个网页 http://localhost:3000/#/swap
部分参考崔棉大师最新的部署视频
当前以uniswap-interface-2.6.0为例
以下替换没有整理顺序,以我这边测试所需要的替换全部列出
当前是部署到BSC测试网例子
增加chainID,替换factory,initcode
//line3,增加BSC 97
export declare enum ChainId {
MAINNET = 1,
ROPSTEN = 3,
RINKEBY = 4,
GÖRLI = 5,
KOVAN = 42,
BSC = 97
}
//line20,替换工厂和initcode
export declare const FACTORY_ADDRESS = "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f";
export declare const INIT_CODE_HASH = "0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f";
增加chainID,替换factory,initcode
增加weth
同目录下sdk.cjs.development.js,如有需要,也一样的方式替换
//line18,增加BSC
(function (ChainId) {
ChainId[ChainId["MAINNET"] = 1] = "MAINNET";
ChainId[ChainId["ROPSTEN"] = 3] = "ROPSTEN";
ChainId[ChainId["RINKEBY"] = 4] = "RINKEBY";
ChainId[ChainId["G\xD6RLI"] = 5] = "G\xD6RLI";
ChainId[ChainId["KOVAN"] = 42] = "KOVAN";
ChainId[ChainId["BSC"] = 97] = "BSC";
})(ChainId || (ChainId = {}));
//替换factory,initcode
var FACTORY_ADDRESS = '0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f';
var INIT_CODE_HASH = '0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f';
//line442,增加对应的weth
var WETH = (_WETH = {}, _WETH[ChainId.MAINNET] = /*#__PURE__*/new Token(ChainId.MAINNET, '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', 18, 'WETH', 'Wrapped Ether'), _WETH[ChainId.ROPSTEN] = /*#__PURE__*/new Token(ChainId.ROPSTEN, '0xc778417E063141139Fce010982780140Aa0cD5Ab', 18, 'WETH', 'Wrapped Ether'), _WETH[ChainId.RINKEBY] = /*#__PURE__*/new Token(ChainId.RINKEBY, '0xc778417E063141139Fce010982780140Aa0cD5Ab', 18, 'WETH', 'Wrapped Ether'), _WETH[ChainId.GÖRLI] = /*#__PURE__*/new Token(ChainId.GÖRLI, '0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6', 18, 'WETH', 'Wrapped Ether'), _WETH[ChainId.KOVAN] = /*#__PURE__*/new Token(ChainId.KOVAN, '0xd0A1E359811322d97991E03f863a0C30C2cF029C', 18, 'WETH', 'Wrapped Ether'), _WETH[ChainId.BSC] = /*#__PURE__*/new Token(ChainId.BSC, '0x替换成自己的WETH地址', 18, 'WETH', 'Wrapped Ether'), _WETH);
WETH
//文件最下面按照格式增加一个weth
{
"name": "Wrapped Ether",
"address": "0x替换自己部署的weth",
"symbol": "WETH",
"decimals": 18,
"chainId": 97,
"logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xd0A1E359811322d97991E03f863a0C30C2cF029C/logo.png"
}
WETH
export declare const WETH: {
1: Token;
3: Token;
4: Token;
5: Token;
42: Token;
97: Token;
};
增加支持的chainID
//line29,支持的网络 增加97
export const injected = new InjectedConnector({
supportedChainIds: [1, 3, 4, 5, 42, 97]
})
替换路由,增加weth
//line6,替换路由地址
export const ROUTER_ADDRESS = '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D'
//line20,增加BSC
const WETH_ONLY: ChainTokenList = {
[ChainId.MAINNET]: [WETH[ChainId.MAINNET]],
[ChainId.ROPSTEN]: [WETH[ChainId.ROPSTEN]],
[ChainId.RINKEBY]: [WETH[ChainId.RINKEBY]],
[ChainId.GÖRLI]: [WETH[ChainId.GÖRLI]],
[ChainId.KOVAN]: [WETH[ChainId.KOVAN]],
[ChainId.BSC]: [WETH[ChainId.BSC]]
}
替换multicall 合约地址
合约代码 https://cn.etherscan.com/address/0xeefBa1e63905eF1D7ACbA5a8513c70307C1cE441#code
直接复制下,部署一个即可
const MULTICALL_NETWORKS: { [chainId in ChainId]: string } = {
[ChainId.MAINNET]: '0xeefBa1e63905eF1D7ACbA5a8513c70307C1cE441',
[ChainId.ROPSTEN]: '0x53C43764255c17BD724F74c4eF150724AC50a3ed',
[ChainId.KOVAN]: '0x2cc8688C5f75E365aaEEb4ea8D6a480405A48D2A',
[ChainId.RINKEBY]: '0x42Ad527de7d4e9d9d011aC45B31D8551f8Fe9821',
[ChainId.GÖRLI]: '0x77dCa2C955b15e9dE4dbBCf1246B4B85b651e50e',
[ChainId.BSC]: '0x替换成自己部署的地址'
}
v1的factory,增加一个空的就好了
const V1_FACTORY_ADDRESSES: { [chainId in ChainId]: string } = {
[ChainId.MAINNET]: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95',
[ChainId.ROPSTEN]: '0x9c83dCE8CA20E9aAF9D3efc003b2ea62aBC08351',
[ChainId.RINKEBY]: '0xf5D915570BC477f9B8D6C0E980aA81757A3AaC36',
[ChainId.GÖRLI]: '0x6Ce570d02D73d4c384b46135E87f8C592A8c86dA',
[ChainId.KOVAN]: '0xD3E51Ef092B2845f10401a0159B2B96e8B6c3D30',
[ChainId.BSC]: ''
}
//line33,增加一个
/**
* An empty result, useful as a default.
*/
const EMPTY_LIST: TokenAddressMap = {
[ChainId.KOVAN]: {},
[ChainId.RINKEBY]: {},
[ChainId.ROPSTEN]: {},
[ChainId.GÖRLI]: {},
[ChainId.MAINNET]: {},
[ChainId.BSC]: {}
}
该修改主要用于交易后的提示hash,可以直接点击到浏览器查看
注意host不要斜杠 / 结尾
//line20-30, 直接用下面的替换
const ETHERSCAN_PREFIXES: { [chainId in ChainId]: string } = {
1: 'https://etherscan.io',
3: 'https://ropsten.etherscan.io',
4: 'https://rinkeby.etherscan.io',
5: 'https://goerli.etherscan.io',
42: 'https://kovan.etherscan.io',
97: 'https://testnet.bscscan.com'
}
export function getEtherscanLink(chainId: ChainId, data: string, type: 'transaction' | 'token' | 'address'): string {
const prefix = `${ETHERSCAN_PREFIXES[chainId] || ETHERSCAN_PREFIXES[1]}`
网页右上角显示网络名称
//line129, ChainId.BSC是在sdk中添加的, 对应的值Bsc只是一个展示
const NETWORK_LABELS: { [chainId in ChainId]: string | null } = {
[ChainId.MAINNET]: null,
[ChainId.RINKEBY]: 'Rinkeby',
[ChainId.ROPSTEN]: 'Ropsten',
[ChainId.GÖRLI]: 'Görli',
[ChainId.KOVAN]: 'Kovan',
[ChainId.BSC]: 'Bsc'
}
如果会solidity,且看uniswap源码的,可以往下看看
崔棉大师有个Uniswap源码中文注解的文档,有需要的可以去购买
添加流动性需要输入两个token, 带ETH的方法,router会帮你转成WETH,最终实际就是该方法
function addLiquidity(
address tokenA,
address tokenB,
uint amountADesired,
uint amountBDesired,
uint amountAMin,
uint amountBMin,
address to,
uint deadline
)
这里主要说明交易的方法
这里主要归纳为3个方法(带ETH的方法,router会帮你转成WETH,最终都是两个ERC20交换)
说明:
//方法1 需要获取精确的输出,输入不确定金额(也可以理解买,如购买100个UNI TOKEN,需要未知weth)
function swapTokensForExactTokens(
uint amountOut,//期望输出金额
uint amountInMax,//最大输入 (如果到你打包的交易时,如果实际需要输入的金额大于该金额,交易失败!)
address[] calldata path,
address to,
uint deadline
)
//方法2 通过输入金额,输出不确定金额 (也可以理解为卖, 如卖掉100个UNI TOKEN,可以获得未知WETH)
function swapExactTokensForTokens(
uint amountIn,//实际输入金额
uint amountOutMin,//最小输出 (如果到打包你的交易时,实际输出小于该金额,交易失败!)
address[] calldata path,
address to,
uint deadline
)
//方法3 通过输入金额,输出不确定金额
function swapExactTokensForTokensSupportingFeeOnTransferTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
)
此处说明上面方法2/3 的区别
方法3是以交易对实际获取到了多少代币,去调用交易对的交换
方法2是以用户调用转账输入的金额(该金额可能是错的,比如没有实际输入,或者扣了手续费),去调用交易对交换
比如黑币,在你转账的时候,扣除你20% 30%等
如果使用方法2 是无法成功 会提示UniswapV2: K
如果使用方法3,把amountOutMin填0,那么这个交易一定可以成功,哪怕交易对只给返回0.00000000001个以太坊
// returns sorted token addresses, used to handle return values from pairs sorted in this order
//两个地址排序
function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) {
require(tokenA != tokenB, 'UniswapV2Library: IDENTICAL_ADDRESSES');
(token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
require(token0 != address(0), 'UniswapV2Library: ZERO_ADDRESS');
}
// calculates the CREATE2 address for a pair without making any external calls
// 计算交易对地址, 注意这个init code hash... 这是个坑
function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) {
(address token0, address token1) = sortTokens(tokenA, tokenB);
pair = address(uint(keccak256(abi.encodePacked(
hex'ff',
factory,
keccak256(abi.encodePacked(token0, token1)),
hex'de683b3097cb455dd2d3ea50f1f95386fdeca75180cc01bb6b12207c44272e17' // init code hash
))));
}
// fetches and sorts the reserves for a pair
//获取当前储备量,返回值会根据你输入的token排序
function getReserves(address factory, address tokenA, address tokenB) internal view returns (uint reserveA, uint reserveB) {
(address token0,) = sortTokens(tokenA, tokenB);
(uint reserve0, uint reserve1,) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves();
(reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
}
// given some amount of an asset and pair reserves, returns an equivalent amount of the other asset
//添加流动性时,通过tokenA输入额,计算tokenB需要输入多少
function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) {
require(amountA > 0, 'UniswapV2Library: INSUFFICIENT_AMOUNT');
require(reserveA > 0 && reserveB > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
amountB = amountA.mul(reserveB) / reserveA;
}
// given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset
//通过in计算out (后面详细说明)
function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {
require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT');
require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
uint amountInWithFee = amountIn.mul(997);
uint numerator = amountInWithFee.mul(reserveOut);
uint denominator = reserveIn.mul(1000).add(amountInWithFee);
amountOut = numerator / denominator;
}
// given an output amount of an asset and pair reserves, returns a required input amount of the other asset
//通过out 计算in (后面详细说明)
function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) internal pure returns (uint amountIn) {
require(amountOut > 0, 'UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT');
require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
uint numerator = reserveIn.mul(amountOut).mul(1000);
uint denominator = reserveOut.sub(amountOut).mul(997);
amountIn = (numerator / denominator).add(1);
}
注:
此处主要说getAmountOut 和 getAmountIn 两个方法
上面工具中的两个方法内部算法是简化过的。
/**
*
*
* 推导公式
* in 输入金额, out 输出金额
* rIn tokenIn的流动性, rOut,tokenOut的流动性
* fee 手续费,注:当前带入0.997 也就是997/1000
*
* 两个计算公式实际是一样的, 只是一个求in,一个求out
* (rIn + in * f) * (rOut - out) = rIn * rOut
*
*
* 由out计算in
* (rIn + in * f) * (rOut - out) = rIn * rOut
* rIn * rOut + in * f * rOut - rIn * out - in * f * out = rIn * rOut
* rIn * out = in * f * rOut - in * f * out
* in = rIn * out / (f * (rOut - out)) + 1 (尾部的 +1应该是避免精度计算,最后一位小了,会成交不了)
*
*
* 由in计算out
* (rIn + in * f) * (rOut - out) = rIn * rOut
* rIn * rOut + in * f * rOut - rIn * out - in * f * out = rIn * rOut
* in * f * rOut = rIn * out + in * f * out
* out = in * f * rOut / rIn + in *f
*
*/
UniswapV2: K 校验手续费
//注:正常amount0 或者amount1有一个是0值
function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {
require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');
uint balance0;
uint balance1;
{ // scope for _token{0,1}, avoids stack too deep errors
address _token0 = token0;
address _token1 = token1;
require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');
//其中一个不是0的转出
if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens
if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens
//闪电贷,略
if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
balance0 = IERC20(_token0).balanceOf(address(this));
balance1 = IERC20(_token1).balanceOf(address(this));
}
//以下代码校验,查看下面说明
uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');
{ // scope for reserve{0,1}Adjusted, avoids stack too deep errors
uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');
}
_update(balance0, balance1, _reserve0, _reserve1);
emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
}
校验K推导说明
针对K值的校验计算
转入金额是实际带手续费的, out金额是in去掉手续费后,计算出来的out.
正常情况 (举例: 0in, 1 out) --即amount0out会是0,不需要转出
(交易前已经将0 in转入)
获取之前的流动性 r0,r1
转出out金额
获取交易对中两个地址的余额 b0,b1
amount0in = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;(0是in,0Out=0,所以前面的比较是true,结果是 )
amount0in = b0>r0 结果是 b0-r0,即实际进入金额(带手续费金额)
amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0; (1是out)
正常是r1-out1=b1 所以走false,
得到 amount1In=0;
一定要这两个其中之一大于0
校验手续费
b0a = b0*1000 - amount0in * 3
b1a = b1*1000 - 0*3
req(b0a*b1a >= r0 * r1 * 1000 * 1000)
r0*r1是上一个k值,
公式 (rIn + in * f) * (rOut - out) = rIn * rOut
(b0-fee)*(b1) = r0*r1(上一次的k)
所以判断条件是(b0-fee)*(b1) >= r0*r1(上一次的k)
对比b0a,b1a 实际就是千3的手续费,没法用小数,所以两边都*1000,就可以用3计算