Skip to content

Commit 23c127f

Browse files
authored
spec: echidna tests for bigint precision
1 parent 6a9fcd9 commit 23c127f

File tree

1 file changed

+187
-0
lines changed

1 file changed

+187
-0
lines changed

src/main.tex

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
\documentclass[runningheads]{llncs}
22
%
33
\usepackage{graphicx}
4+
\usepackage{minted}
45
%
56
% If you use the hyperref package, please uncomment the following line
67
% to display URLs in blue roman font according to Springer's eBook style:
@@ -239,6 +240,192 @@ \section{Liquidity Execution}
239240

240241
\section{Equivalence Checking}
241242

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+
242429
\section{Benchmarking}
243430

244431
\section{Use Cases and Experiments}

0 commit comments

Comments
 (0)