|
1 | 1 | \documentclass[runningheads]{llncs} |
2 | 2 | % |
3 | 3 | \usepackage{graphicx} |
| 4 | +\usepackage{minted} |
4 | 5 | % |
5 | 6 | % If you use the hyperref package, please uncomment the following line |
6 | 7 | % to display URLs in blue roman font according to Springer's eBook style: |
@@ -239,6 +240,192 @@ \section{Liquidity Execution} |
239 | 240 |
|
240 | 241 | \section{Equivalence Checking} |
241 | 242 |
|
| 243 | +Backrun placement |
| 244 | + |
| 245 | +By definition, backruns must occur after user to user swap. From a design point of view the simplest place to insert the backrun function would be in the internal `_swap` function which is called by the other swaps. However, some of the swap variants eg `swapTokensForExactETH` perform user actions after `_swap` is called. This is not ideal, as we do not want to interfere with the user swap. Moreover, other swap variants such as `swapExactTokensForTokensSupportingFeeOnTransferTokens` do not use `_swap`. Backrun functions were therefore placed at the end of each external swap variant. E.g. |
| 246 | + |
| 247 | +Original |
| 248 | +\begin{minted}{javascript}{javascript} |
| 249 | +\label{Before swapExactTokensforTokens:1} |
| 250 | + function swapExactTokensForTokens( |
| 251 | + uint amountIn, |
| 252 | + uint amountOutMin, |
| 253 | + address[] calldata path, |
| 254 | + address to, |
| 255 | + uint deadline |
| 256 | + ) external virtual override ensure(deadline) returns (uint[] memory amounts) { |
| 257 | + amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path); |
| 258 | + require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'); |
| 259 | + TransferHelper.safeTransferFrom( |
| 260 | + path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0] |
| 261 | + ); |
| 262 | + _swap(amounts, path, to); |
| 263 | + } |
| 264 | +\end{minted} |
| 265 | + |
| 266 | +Design change: |
| 267 | + |
| 268 | +\begin{minted}{javascript} |
| 269 | +\label{After swapExactTokensforTokens:2} |
| 270 | +function swapExactTokensForTokens( |
| 271 | + uint amountIn, |
| 272 | + uint amountOutMin, |
| 273 | + address[] calldata path, |
| 274 | + address to, |
| 275 | + uint deadline |
| 276 | +) external virtual override ensure(deadline) returns (uint[] memory amounts) { |
| 277 | + amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path); |
| 278 | + require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'); |
| 279 | + TransferHelper.safeTransferFrom( |
| 280 | + path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0] |
| 281 | + ); |
| 282 | + _swap(amounts, path, to); |
| 283 | + _backrunSwaps(path); |
| 284 | +} |
| 285 | +\end{minted} |
| 286 | + |
| 287 | +Multiple factories |
| 288 | +Multiple factories (at least 2) are required for the backrun arbitrage. The adoption of multiple factories within the router, lead to some internal function changes. In particular `pairFor`. |
| 289 | + |
| 290 | +\begin{minted}{javascript} |
| 291 | +\label{CREATE2 Factory:3} |
| 292 | +// calculates the CREATE2 address for a pair without making any external calls |
| 293 | +function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) { |
| 294 | +(address token0, address token1) = sortTokens(tokenA, tokenB); |
| 295 | +pair = address(uint160(uint(keccak256(abi.encodePacked( |
| 296 | + hex'ff', |
| 297 | + factory, |
| 298 | + keccak256(abi.encodePacked(token0, token1)), |
| 299 | + hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f' // hard coded factory init code hash |
| 300 | + ))))); |
| 301 | +} |
| 302 | +\end{minted} |
| 303 | + |
| 304 | +Changes to |
| 305 | +\begin{minted}{javascript} |
| 306 | + |
| 307 | +function pairFor(address factory, address tokenA, address tokenB) internal view returns (address pair) { |
| 308 | + bytes memory factoryHash = factory == SUSHI_FACTORY ? SUSHI_FACTORY_HASH : BACKUP_FACTORY_HASH; |
| 309 | + |
| 310 | +(address token0, address token1) = _sortTokens(tokenA, tokenB); |
| 311 | +pair = address(uint160(uint(keccak256(abi.encodePacked( |
| 312 | + hex'ff', |
| 313 | + factory, |
| 314 | + keccak256(abi.encodePacked(token0, token1)), |
| 315 | + factoryHash // init code hash |
| 316 | + ))))); |
| 317 | +} |
| 318 | +\end{minted} |
| 319 | + |
| 320 | +Fallback factory |
| 321 | +Since the extra factory is required for the arbitrage, we can use it, for the user, to check for an available swap on the alternate factory if it would otherwise fail on the default factory through slippage. |
| 322 | + |
| 323 | + |
| 324 | +\begin{minted}{javascript} |
| 325 | +\label{Fallback Factory:4} |
| 326 | +amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path); |
| 327 | +require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'); |
| 328 | +\end{minted} |
| 329 | +Changes to |
| 330 | +\begin{minted}{javascript} |
| 331 | +address factory = SUSHI_FACTORY; |
| 332 | +amounts = _getAmountsOut(factory, amountIn, path); |
| 333 | +if(amounts[amounts.length - 1] < amountOutMin){ |
| 334 | + // Change 1 -> fallback for insufficient output amount, check backup router |
| 335 | + amounts = _getAmountsOut(BACKUP_FACTORY, amountIn, path); |
| 336 | + require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'); |
| 337 | + factory = BACKUP_FACTORY; |
| 338 | +} |
| 339 | +\end{minted} |
| 340 | + |
| 341 | + |
| 342 | +Uint256 overflow |
| 343 | +Optimal arbitrage calculations were overflowing uint256. e.g. |
| 344 | +\begin{minted}{javascript} |
| 345 | +\label{Uint256 overflow:5} |
| 346 | +uint Cd = reserve0Token1.mul(997).mul(997); |
| 347 | +uint Cc = reserve1Token1.mul(997000); |
| 348 | +uint Cb = reserve1Token1.mul(reserve0Token0).mul(1000).mul(1000); |
| 349 | +uint Ca = reserve1Token0.mul(reserve0Token1).mul(997).mul(997); |
| 350 | +uint Cf = Ca - Cb; |
| 351 | +uint Cg = Cc + Cd; |
| 352 | +uint a = Cg * Cg; |
| 353 | +uint b = 2 * Cb * Cg; |
| 354 | +uint c = Cb * Cf; |
| 355 | +uint d = (b*b) + ( 4 * a * c ); |
| 356 | +\end{minted} |
| 357 | + |
| 358 | +would consistently overflow by `uint b`. Found out through individual checks: |
| 359 | +\begin{minted}{javascript} |
| 360 | +unchecked { |
| 361 | + uint a = Cg * Cg; |
| 362 | + require(a/Cg == Cg,"a overflow"); |
| 363 | + uint b = 2 * Cb * Cg; |
| 364 | + require(b/Cb == 2*Cg ,"b overflow"); |
| 365 | + uint c = Cb * Cf; |
| 366 | + require(c/Cb == Cf,"c overflow"); |
| 367 | + uint d = (b*b) + ( 4 * a * c ); |
| 368 | + require(d/(b*b) == 4*a*c,"d overflow"); |
| 369 | +} |
| 370 | +\end{minted} |
| 371 | + |
| 372 | +ABDKMath \footnote{https://github.com/abdk-consulting/abdk-libraries-solidity} libs were used for a time, as it avoided overflow by dropping to floats. E.g. |
| 373 | + |
| 374 | +\begin{minted}{javascript} |
| 375 | +bytes16 _Cg = ABDKMathQuad.fromUInt(Cg); |
| 376 | +bytes16 _a = ABDKMathQuad.mul(_Cg, _Cg); |
| 377 | +\end{minted} |
| 378 | + |
| 379 | +However we found this lost precision and failed echidna tests. |
| 380 | +\begin{minted}{javascript} |
| 381 | +\label{Uint256 overflow:5} |
| 382 | +\caption{{\em echidna} test} |
| 383 | +echidna_mulUint: failed! |
| 384 | + Call sequence: |
| 385 | + setX1(1106235220955) |
| 386 | + setX(9390953368914254812617) |
| 387 | + |
| 388 | + |
| 389 | +echidna_Uint_convertion: failed! |
| 390 | + Call sequence: |
| 391 | + setX(10518526264810785485368401065708505) |
| 392 | + |
| 393 | + |
| 394 | +echidna_divUint: failed! |
| 395 | + Call sequence: |
| 396 | + setX(10417774989007224423389698535018119) |
| 397 | + setX1(1) |
| 398 | +\end{minted} |
| 399 | + |
| 400 | +We also tried PRBMath\footnote{https://github.com/paulrberg/prb-math/} libs. These faired better in echidna tests but still suffered overflow issues. |
| 401 | + |
| 402 | +\begin{minted}{javascript} |
| 403 | +echidna_mulUint: failed! |
| 404 | + Call sequence: |
| 405 | + setX(115916773041390072873637598212453390567932363729484377996870) |
| 406 | + |
| 407 | + |
| 408 | +echidna_Uint_convertion: failed! |
| 409 | + Call sequence: |
| 410 | + setX(115962837499224411198969207499961588040517688084412876519766) |
| 411 | + |
| 412 | + |
| 413 | +echidna_divUint: failed! |
| 414 | + Call sequence: |
| 415 | + setX(115989750869986627937072895547572258287879165164826483095329) |
| 416 | + setX1(1) |
| 417 | +\end{minted} |
| 418 | + |
| 419 | +Eventually we found Uint512\footnote{https://github.com/SimonSuckut/Solidity_Uint512/blob/main/contracts/Uint512.sol} which both passed echidna and overflow issue. |
| 420 | +\begin{minted}{javascript} |
| 421 | +echidna_mulUint: passed! |
| 422 | +echidna_divUint: passed! |
| 423 | +\end{minted} |
| 424 | + |
| 425 | + |
| 426 | + |
| 427 | +https://github.com/SimonSuckut/Solidity_Uint512/blob/main/contracts/Uint512.sol |
| 428 | + |
242 | 429 | \section{Benchmarking} |
243 | 430 |
|
244 | 431 | \section{Use Cases and Experiments} |
|
0 commit comments