Skip to content

Commit

Permalink
optimistic swaps (Uniswap#53)
Browse files Browse the repository at this point in the history
* alternative flash lending (renting) design

* add rent interface

* fix stack too deep error

rearrange order of k condition math

ignore erroneous out of gas errors in tests

* try removing rent in favor of monolithic swap

IUniswapV2Borrower -> IUniswapV2Callee

update tests

* fix implementation

* clean up math a bit

* amount{0,1}In -> amount{0,1}InNet

* charge on all inputs, not just net

* removed unnecessary safemath

* add to != token check

don't indent in scope

rename reserve{0,1}Next -> reserve{0,1}Adjusted

* > instead of >=

simplify algebra

reserve{0,1}Adjusted -> balance{0,1}Adjusted

add comments

* add some optimistic swap test cases
  • Loading branch information
NoahZinsmeister authored Feb 17, 2020
1 parent 3f5feaa commit b742e92
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 49 deletions.
64 changes: 37 additions & 27 deletions contracts/UniswapV2Exchange.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import './libraries/Math.sol';
import './libraries/UQ112x112.sol';
import './interfaces/IERC20.sol';
import './interfaces/IUniswapV2Factory.sol';
import './interfaces/IUniswapV2Callee.sol';

contract UniswapV2Exchange is IUniswapV2Exchange, UniswapV2ERC20 {
using SafeMath for uint;
Expand Down Expand Up @@ -49,13 +50,21 @@ contract UniswapV2Exchange is IUniswapV2Exchange, UniswapV2ERC20 {

event Mint(address indexed sender, uint amount0, uint amount1);
event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);
event Swap(address indexed sender, address indexed tokenIn, uint amountIn, uint amountOut, address indexed to);
event Swap(
address indexed sender,
uint amount0In,
uint amount1In,
uint amount0Out,
uint amount1Out,
address indexed to
);
event Sync(uint112 reserve0, uint112 reserve1);

constructor() public {
factory = msg.sender;
}

// called once by the factory at time of deployment
function initialize(address _token0, address _token1) external {
require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check
token0 = _token0;
Expand Down Expand Up @@ -99,6 +108,7 @@ contract UniswapV2Exchange is IUniswapV2Exchange, UniswapV2ERC20 {
}
}

// this low-level function should be called from a contract which performs important safety checks
function mint(address to) external lock returns (uint liquidity) {
(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
uint balance0 = IERC20(token0).balanceOf(address(this));
Expand All @@ -118,10 +128,11 @@ contract UniswapV2Exchange is IUniswapV2Exchange, UniswapV2ERC20 {
_mint(to, liquidity);

_update(balance0, balance1, _reserve0, _reserve1);
if (feeOn) kLast = uint(reserve0).mul(reserve1);
if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
emit Mint(msg.sender, amount0, amount1);
}

// this low-level function should be called from a contract which performs important safety checks
function burn(address to) external lock returns (uint amount0, uint amount1) {
(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
address _token0 = token0; // gas savings
Expand All @@ -142,40 +153,39 @@ contract UniswapV2Exchange is IUniswapV2Exchange, UniswapV2ERC20 {
balance1 = IERC20(_token1).balanceOf(address(this));

_update(balance0, balance1, _reserve0, _reserve1);
if (feeOn) kLast = uint(reserve0).mul(reserve1);
if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
emit Burn(msg.sender, amount0, amount1, to);
}

function swap(address tokenIn, uint amountOut, address to) external lock {
require(amountOut > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
// this low-level function should be called from a contract which performs important safety checks
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
address _token0 = token0; // gas savings
address _token1 = token1; // gas savings
require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');

uint balance0;
uint balance1;
uint amountIn;

if (tokenIn == _token0) {
require(amountOut < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');
balance0 = IERC20(_token0).balanceOf(address(this));
amountIn = balance0.sub(_reserve0);
require(amountIn > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');
require(amountIn.mul(_reserve1 - amountOut).mul(997) >= amountOut.mul(_reserve0).mul(1000), 'UniswapV2: K');
_safeTransfer(_token1, to, amountOut);
balance1 = IERC20(_token1).balanceOf(address(this));
} else {
require(tokenIn == _token1, 'UniswapV2: INVALID_INPUT_TOKEN');
require(amountOut < _reserve0, 'UniswapV2: INSUFFICIENT_LIQUIDITY');
balance1 = IERC20(_token1).balanceOf(address(this));
amountIn = balance1.sub(_reserve1);
require(amountIn > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');
require(amountIn.mul(_reserve0 - amountOut).mul(997) >= amountOut.mul(_reserve1).mul(1000), 'UniswapV2: K');
_safeTransfer(_token0, to, amountOut);
balance0 = IERC20(_token0).balanceOf(address(this));
{ // scope for _token{0,1}, avoids stack too deep errors
address _token0 = token0;
address _token1 = token1;
require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');
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, tokenIn, amountIn, amountOut, to);
emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
}

// force balances to match reserves
Expand Down
5 changes: 5 additions & 0 deletions contracts/interfaces/IUniswapV2Callee.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pragma solidity =0.5.16;

interface IUniswapV2Callee {
function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external;
}
11 changes: 9 additions & 2 deletions contracts/interfaces/IUniswapV2Exchange.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,14 @@ interface IUniswapV2Exchange {

event Mint(address indexed sender, uint amount0, uint amount1);
event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);
event Swap(address indexed sender, address indexed tokenIn, uint amountIn, uint amountOut, address indexed to);
event Swap(
address indexed sender,
uint amount0In,
uint amount1In,
uint amount0Out,
uint amount1Out,
address indexed to
);
event Sync(uint112 reserve0, uint112 reserve1);

function MINIMUM_LIQUIDITY() external pure returns (uint);
Expand All @@ -37,7 +44,7 @@ interface IUniswapV2Exchange {

function mint(address to) external returns (uint liquidity);
function burn(address to) external returns (uint amount0, uint amount1);
function swap(address tokenIn, uint amountOut, address to) external;
function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external;
function skim(address to) external;
function sync() external;

Expand Down
51 changes: 35 additions & 16 deletions test/UniswapV2Exchange.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const MINIMUM_LIQUIDITY = bigNumberify(10).pow(3)
chai.use(solidity)

const overrides = {
gasLimit: 1000000
gasLimit: 9999999
}

describe('UniswapV2Exchange', () => {
Expand Down Expand Up @@ -68,7 +68,7 @@ describe('UniswapV2Exchange', () => {
await token1.transfer(exchange.address, token1Amount)
await exchange.mint(wallet.address, overrides)
}
const testCases: BigNumber[][] = [
const swapTestCases: BigNumber[][] = [
[1, 5, 10, '1662497915624478906'],
[1, 10, 5, '453305446940074565'],

Expand All @@ -78,15 +78,34 @@ describe('UniswapV2Exchange', () => {
[1, 10, 10, '906610893880149131'],
[1, 100, 100, '987158034397061298'],
[1, 1000, 1000, '996006981039903216']
].map(a => a.map((n, i) => (i === 3 ? bigNumberify(n) : expandTo18Decimals(n as number))))
testCases.forEach((testCase, i) => {
].map(a => a.map(n => (typeof n === 'string' ? bigNumberify(n) : expandTo18Decimals(n))))
swapTestCases.forEach((swapTestCase, i) => {
it(`getInputPrice:${i}`, async () => {
await addLiquidity(testCase[1], testCase[2])
await token0.transfer(exchange.address, testCase[0])
await expect(exchange.swap(token0.address, testCase[3].add(1), wallet.address, overrides)).to.be.revertedWith(
const [swapAmount, token0Amount, token1Amount, expectedOutputAmount] = swapTestCase
await addLiquidity(token0Amount, token1Amount)
await token0.transfer(exchange.address, swapAmount)
await expect(exchange.swap(0, expectedOutputAmount.add(1), wallet.address, '0x', overrides)).to.be.revertedWith(
'UniswapV2: K'
)
await exchange.swap(token0.address, testCase[3], wallet.address, overrides)
await exchange.swap(0, expectedOutputAmount, wallet.address, '0x', overrides)
})
})

const optimisticTestCases: BigNumber[][] = [
['997000000000000000', 5, 10, 1], // given amountIn, amountOut = floor(amountIn * .997)
['997000000000000000', 10, 5, 1],
['997000000000000000', 5, 5, 1],
[1, 5, 5, '1003009027081243732'] // given amountOut, amountIn = ceiling(amountOut / .997)
].map(a => a.map(n => (typeof n === 'string' ? bigNumberify(n) : expandTo18Decimals(n))))
optimisticTestCases.forEach((optimisticTestCase, i) => {
it(`optimistic:${i}`, async () => {
const [outputAmount, token0Amount, token1Amount, inputAmount] = optimisticTestCase
await addLiquidity(token0Amount, token1Amount)
await token0.transfer(exchange.address, inputAmount)
await expect(exchange.swap(outputAmount.add(1), 0, wallet.address, '0x', overrides)).to.be.revertedWith(
'UniswapV2: K'
)
await exchange.swap(outputAmount, 0, wallet.address, '0x', overrides)
})
})

Expand All @@ -98,13 +117,13 @@ describe('UniswapV2Exchange', () => {
const swapAmount = expandTo18Decimals(1)
const expectedOutputAmount = bigNumberify('1662497915624478906')
await token0.transfer(exchange.address, swapAmount)
await expect(exchange.swap(token0.address, expectedOutputAmount, wallet.address, overrides))
await expect(exchange.swap(0, expectedOutputAmount, wallet.address, '0x', overrides))
.to.emit(token1, 'Transfer')
.withArgs(exchange.address, wallet.address, expectedOutputAmount)
.to.emit(exchange, 'Sync')
.withArgs(token0Amount.add(swapAmount), token1Amount.sub(expectedOutputAmount))
.to.emit(exchange, 'Swap')
.withArgs(wallet.address, token0.address, swapAmount, expectedOutputAmount, wallet.address)
.withArgs(wallet.address, swapAmount, 0, 0, expectedOutputAmount, wallet.address)

const reserves = await exchange.getReserves()
expect(reserves[0]).to.eq(token0Amount.add(swapAmount))
Expand All @@ -125,13 +144,13 @@ describe('UniswapV2Exchange', () => {
const swapAmount = expandTo18Decimals(1)
const expectedOutputAmount = bigNumberify('453305446940074565')
await token1.transfer(exchange.address, swapAmount)
await expect(exchange.swap(token1.address, expectedOutputAmount, wallet.address, overrides))
await expect(exchange.swap(expectedOutputAmount, 0, wallet.address, '0x', overrides))
.to.emit(token0, 'Transfer')
.withArgs(exchange.address, wallet.address, expectedOutputAmount)
.to.emit(exchange, 'Sync')
.withArgs(token0Amount.sub(expectedOutputAmount), token1Amount.add(swapAmount))
.to.emit(exchange, 'Swap')
.withArgs(wallet.address, token1.address, swapAmount, expectedOutputAmount, wallet.address)
.withArgs(wallet.address, 0, swapAmount, expectedOutputAmount, 0, wallet.address)

const reserves = await exchange.getReserves()
expect(reserves[0]).to.eq(token0Amount.sub(expectedOutputAmount))
Expand All @@ -157,7 +176,7 @@ describe('UniswapV2Exchange', () => {
const expectedOutputAmount = bigNumberify('453305446940074565')
await token0.transfer(exchange.address, swapAmount)
await mineBlock(provider, (await provider.getBlock('latest')).timestamp + 1)
const gasCost = await exchange.estimate.swap(token0.address, expectedOutputAmount, wallet.address, overrides)
const gasCost = await exchange.estimate.swap(0, expectedOutputAmount, wallet.address, '0x', overrides)
console.log(`Gas required for swap: ${gasCost}`)
})

Expand Down Expand Up @@ -209,7 +228,7 @@ describe('UniswapV2Exchange', () => {
await token0.transfer(exchange.address, swapAmount)
await mineBlock(provider, blockTimestamp + 10)
// swap to a new price eagerly instead of syncing
await exchange.swap(token0.address, expandTo18Decimals(1), wallet.address, overrides) // make the price nice
await exchange.swap(0, expandTo18Decimals(1), wallet.address, '0x', overrides) // make the price nice

expect(await exchange.price0CumulativeLast()).to.eq(initialPrice[0].mul(10))
expect(await exchange.price1CumulativeLast()).to.eq(initialPrice[1].mul(10))
Expand All @@ -232,7 +251,7 @@ describe('UniswapV2Exchange', () => {
const swapAmount = expandTo18Decimals(1)
const expectedOutputAmount = bigNumberify('996006981039903216')
await token1.transfer(exchange.address, swapAmount)
await exchange.swap(token1.address, expectedOutputAmount, wallet.address, overrides)
await exchange.swap(expectedOutputAmount, 0, wallet.address, '0x', overrides)

const expectedLiquidity = expandTo18Decimals(1000)
await exchange.transfer(exchange.address, expectedLiquidity.sub(MINIMUM_LIQUIDITY))
Expand All @@ -250,7 +269,7 @@ describe('UniswapV2Exchange', () => {
const swapAmount = expandTo18Decimals(1)
const expectedOutputAmount = bigNumberify('996006981039903216')
await token1.transfer(exchange.address, swapAmount)
await exchange.swap(token1.address, expectedOutputAmount, wallet.address, overrides)
await exchange.swap(expectedOutputAmount, 0, wallet.address, '0x', overrides)

const expectedLiquidity = expandTo18Decimals(1000)
await exchange.transfer(exchange.address, expectedLiquidity.sub(MINIMUM_LIQUIDITY))
Expand Down
12 changes: 8 additions & 4 deletions test/shared/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,12 @@ interface FactoryFixture {
factory: Contract
}

const overrides = {
gasLimit: 9999999
}

export async function factoryFixture(_: Web3Provider, [wallet]: Wallet[]): Promise<FactoryFixture> {
const factory = await deployContract(wallet, UniswapV2Factory, [wallet.address])
const factory = await deployContract(wallet, UniswapV2Factory, [wallet.address], overrides)
return { factory }
}

Expand All @@ -26,10 +30,10 @@ interface ExchangeFixture extends FactoryFixture {
export async function exchangeFixture(provider: Web3Provider, [wallet]: Wallet[]): Promise<ExchangeFixture> {
const { factory } = await factoryFixture(provider, [wallet])

const tokenA = await deployContract(wallet, ERC20, [expandTo18Decimals(10000)])
const tokenB = await deployContract(wallet, ERC20, [expandTo18Decimals(10000)])
const tokenA = await deployContract(wallet, ERC20, [expandTo18Decimals(10000)], overrides)
const tokenB = await deployContract(wallet, ERC20, [expandTo18Decimals(10000)], overrides)

await factory.createExchange(tokenA.address, tokenB.address)
await factory.createExchange(tokenA.address, tokenB.address, overrides)
const exchangeAddress = await factory.getExchange(tokenA.address, tokenB.address)
const exchange = new Contract(exchangeAddress, JSON.stringify(UniswapV2Exchange.abi), provider).connect(wallet)

Expand Down

0 comments on commit b742e92

Please sign in to comment.