Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Uniswap v3 multihop pricefeed #103

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ There are 4 modes available with a 5th on the way.
* `chainlink` : Follows an external price oracle. Chainlink is WEB3 and might be slower then cryptowatch.
* `constant`: Sets an fixed price and market makes around that price. Can be combined with single-sided liquidity to simulate limit orders.
* `uniswapV3`: Reads prices on-chain from a specified uniswapV3 pool
* `uniswapV3Multihop`: Reads prices on-chain from a specified swap-route of uniswapV3 pools
* `independent`: Under development. The price is set independent of a price feed.

**Warning:** Make sure your price feed is close to the price you see on zigzag. **Otherwise, your mm can lose money!**
Expand Down Expand Up @@ -181,6 +182,16 @@ You can get the available market contracts [here.](https://info.uniswap.org) Sel
}
```

###### UniswapV3Multihop
If there is no direct uniswap pair deployed for the given token pair you can provide a path of pair addresses (separated by `-`) from which to calculate the price.
In the below example the pair addresses for the `icETH/WETH` and `WETH/USDC` pairs are provided to calculate the `icETH-USDC` price.
```
"icETH-USDC": {
"priceFeedPrimary": "uniswapv3Multihop:0xe5d028350093a743A9769e6FD7F5546eEdDAA320-0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640",
....
}
```

###### Constant
With constant mode, you can set a fixed price to market make. The bot will not change that price. Any secondary price feed will be ignored, if used as priceFeedPrimary. Also good as a `priceFeedSecondary` on stablecoins.

Expand Down
157 changes: 150 additions & 7 deletions marketmaker.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ const WALLETS = {};
const MARKETS = {};
const CHAINLINK_PROVIDERS = {};
const UNISWAP_V3_PROVIDERS = {};
const UNISWAP_V3_MULTIHOP_PROVIDERS = {};
const PAST_ORDER_LIST = {};
const FEE_TOKEN_LIST = [];
let FEE_TOKEN = null;

let uniswap_error_counter = 0;
let uniswap_multihop_error_counter = 0;
let chainlink_error_counter = 0;

// Load MM config
Expand Down Expand Up @@ -511,7 +513,10 @@ async function fillOpenOrders() {
}

async function setupPriceFeeds() {
const cryptowatch = [], chainlink = [], uniswapV3 = [];
const cryptowatch = [],
chainlink = [],
uniswapV3 = [],
uniswapV3Multihop = [];
for (let market in MM_CONFIG.pairs) {
const pairConfig = MM_CONFIG.pairs[market];
if(!pairConfig.active) { continue; }
Expand Down Expand Up @@ -540,11 +545,20 @@ async function setupPriceFeeds() {
case 'cryptowatch':
if(!cryptowatch.includes(id)) { cryptowatch.push(id); }
break;
case 'chainlink':
if(!chainlink.includes(id)) { chainlink.push(id); }
case "chainlink":
if (!chainlink.includes(id)) {
chainlink.push(id);
}
break;
case 'uniswapv3':
if(!uniswapV3.includes(id)) { uniswapV3.push(id); }
case "uniswapv3":
if (!uniswapV3.includes(id)) {
uniswapV3.push(id.toLowerCase());
}
break;
case "uniswapv3multihop":
if (!uniswapV3Multihop.includes(id)) {
uniswapV3Multihop.push(id.toLowerCase());
}
break;
case 'constant':
PRICE_FEEDS['constant:'+id] = parseFloat(id);
Expand All @@ -558,6 +572,7 @@ async function setupPriceFeeds() {
if(chainlink.length > 0) await chainlinkSetup(chainlink);
if(cryptowatch.length > 0) await cryptowatchWsSetup(cryptowatch);
if(uniswapV3.length > 0) await uniswapV3Setup(uniswapV3);
if(uniswapV3Multihop.length > 0) await uniswapV3MultihopSetup(uniswapV3Multihop);

console.log(PRICE_FEEDS);
}
Expand Down Expand Up @@ -651,8 +666,136 @@ async function chainlinkUpdate() {
} catch (err) {
chainlink_error_counter += 1;
console.log(`Failed to update chainlink, retry: ${err.message}`);
if(chainlink_error_counter > 4) {
throw new Error ("Failed to update chainlink since 150 seconds!")
if (chainlink_error_counter > 4) {
throw new Error("Failed to update chainlink since 150 seconds!");
}
}
}

async function uniswapV3MultihopSetup(uniswapV3Address) {
const results = uniswapV3Address.map(async (addressesString) => {
try {
const IUniswapV3PoolABI = JSON.parse(
fs.readFileSync("ABIs/IUniswapV3Pool.abi")
);
const ERC20ABI = JSON.parse(fs.readFileSync("ABIs/ERC20.abi"));

const addresses = addressesString.split("-");
const providers = addresses.map((address) => {
return {
pair: new ethers.Contract(
address,
IUniswapV3PoolABI,
ethersProvider
),
reverted: false,
};
});

let curToken = null;
// Check if any of the pairs are reverted
for (const provider of providers) {
const token0 = await provider.pair.token0();
const token1 = await provider.pair.token1();
if (curToken == null) {
curToken = token1;
} else {
if (curToken == token0) {
curToken = token1;
} else {
if (curToken == token1) {
provider.reverted = true;
curToken = token0;
} else {
throw new Error("Tokens don't match");
}
}
}
}

const lastProvider = providers[providers.length - 1];
let [addressToken0, addressToken1] = await Promise.all([
providers[0].pair.token0(),
lastProvider.reverted ? lastProvider.pair.token0() : lastProvider.pair.token1(),
]);

const tokenProvider0 = new ethers.Contract(
addressToken0,
ERC20ABI,
ethersProvider
);
const tokenProvider1 = new ethers.Contract(
addressToken1,
ERC20ABI,
ethersProvider
);

let [decimals0, decimals1] = await Promise.all([
tokenProvider0.decimals(),
tokenProvider1.decimals(),
]);
const decimalsRatio = 10 ** decimals0 / 10 ** decimals1;

const key = "uniswapv3multihop:" + addressesString;
UNISWAP_V3_MULTIHOP_PROVIDERS[key] = [providers, decimalsRatio];

const price = await calculatePriceMultihop(
providers,
decimalsRatio
);
PRICE_FEEDS[key] = price;
} catch (e) {
throw new Error(
"Error while setting up uniswapV3 for " +
addressesString +
", Error: " +
e
);
}
});
await Promise.all(results);
setInterval(uniswapV3MultihopUpdate, 30000);
}

async function calculatePriceMultihop(providers, decimalsRatio) {
const slots = await Promise.all(
providers.map(async (provider) => {
return { slot0: await provider.pair.slot0(), reverted: provider.reverted };
})
);
// get inital price
const price = slots.reduce((price, slot) => {
const { reverted, slot0 } = slot;
const pairPrice = (slot0.sqrtPriceX96 * slot0.sqrtPriceX96);
const x96Adjustment = 2 ** 192;
const nextPrice = reverted ? price * x96Adjustment / pairPrice : price * pairPrice / x96Adjustment;
return nextPrice;
}, 1);
const priceAdjusted = price * decimalsRatio;
return priceAdjusted;
}

async function uniswapV3MultihopUpdate() {
try {
await Promise.all(
Object.keys(UNISWAP_V3_MULTIHOP_PROVIDERS).map(async (key) => {
const [providers, decimalsRatio] =
UNISWAP_V3_MULTIHOP_PROVIDERS[key];
const newPrice = await calculatePriceMultihop(
providers,
decimalsRatio
);
PRICE_FEEDS[key] = newPrice;
})
);
// reset error counter if successful
uniswap_multihop_error_counter = 0;
} catch (err) {
uniswap_multihop_error_counter += 1;
console.log(`Failed to update uniswap, retry: ${err.message}`);
console.log(err.message);
if (uniswap_multihop_error_counter > 4) {
throw new Error("Failed to update uniswap since 150 seconds!");
}
}
}
Expand Down