From e5b8db9df1659d320c1c0bb5ce7c04c6d14c9c42 Mon Sep 17 00:00:00 2001 From: Noah Zinsmeister Date: Fri, 25 Oct 2019 15:06:06 -0400 Subject: [PATCH] initial exchange re-write add initialization function to exchanges add mint function in erc20 --- contracts/UniswapV2.sol | 294 +++++++++--------- contracts/UniswapV2Factory.sol | 5 +- ...per.sol => UniswapV2Helper.sol.unreviewed} | 1 + contracts/token/ERC20.sol | 12 +- 4 files changed, 165 insertions(+), 147 deletions(-) rename contracts/{UniswapV2Helper.sol => UniswapV2Helper.sol.unreviewed} (99%) diff --git a/contracts/UniswapV2.sol b/contracts/UniswapV2.sol index e38458b..2e95e2e 100644 --- a/contracts/UniswapV2.sol +++ b/contracts/UniswapV2.sol @@ -1,178 +1,186 @@ // TODO overflow counter, review, fee pragma solidity 0.5.12; +import "./interfaces/IERC20.sol"; + import "./libraries/Math.sol"; import "./libraries/SafeMath.sol"; -import "./interfaces/IERC20.sol"; + import "./token/ERC20.sol"; contract UniswapV2 is ERC20("Uniswap V2", "UNI-V2", 18, 0) { using Math for uint256; using SafeMath for uint256; - event Swap(address inputToken, address buyer, address recipient, uint256 amountSold, uint256 amountBought); - event AddLiquidity(address indexed provider, uint256 amountTokenA, uint256 amountTokenB); - event RemoveLiquidity(address indexed provider, uint256 amountTokenA, uint256 amountTokenB); + event Swap( + address indexed input, + address indexed sender, + address indexed recipient, + uint256 amountInput, + uint256 amountOutput + ); + event LiquidityMinted( + address indexed sender, + address indexed recipient, + uint256 amountToken0, + uint256 amountToken1 + ); + event LiquidityBurned( + address indexed sender, + address indexed recipient, + uint256 amountToken0, + uint256 amountToken1 + ); struct TokenData { - uint128 reserve; // cached reserve for this token - uint128 accumulator; // accumulated TWAP value (TODO) + uint128 reserve; + uint128 accumulator; } - // TODO: add overflow counter? struct LastUpdate { - uint128 time; - uint128 blockNumber; + uint64 blockNumber; + uint64 blockTimestamp; // overflows about 280 billion years after the earth's sun explodes } - // ERC20 Data - address public tokenA; // ERC20 token traded on this contract - address public tokenB; // ERC20 token traded on this contract - address public factory; // factory that created this contract + bool public initialized; + bool private locked; + + address public factory; + address public token0; + address public token1; - mapping (address => TokenData) public dataForToken; // cached information about the token + mapping (address => TokenData) private tokenData; + LastUpdate private lastUpdate; - LastUpdate public lastUpdate; // information about the last time the reserves were updated - - bool private reentrancyLock = false; - modifier nonReentrant() { - require(!reentrancyLock, "Uniswap: REENTRANCY_FORBIDDEN"); - reentrancyLock = true; + modifier lock() { + require(!locked, "UniswapV2: LOCKED"); + locked = true; _; - reentrancyLock = false; + locked = false; } constructor() public { factory = msg.sender; - // tokenA = _tokenA; - // tokenB = _tokenB; } - function () external {} + function initialize(address _token0, address _token1) public { + require(!initialized, "UniswapV2: ALREADY_INITIALIZED"); + initialized = true; - // TODO public? - function getInputPrice(uint256 inputAmount, uint256 inputReserve, uint256 outputReserve) internal pure returns (uint256) { - require(inputReserve > 0 && outputReserve > 0, "Uniswap: INVALID_VALUE"); - uint256 inputAmountWithFee = inputAmount.mul(997); - uint256 numerator = inputAmountWithFee.mul(outputReserve); - uint256 denominator = inputReserve.mul(1000).add(inputAmountWithFee); - return numerator / denominator; + token0 = _token0; + token1 = _token1; } - function updateData( - address firstToken, - address secondToken, - TokenData memory oldFirstTokenData, - TokenData memory oldSecondTokenData, - uint128 newFirstTokenReserve, - uint128 newSecondTokenReserve - ) internal returns (uint128, uint128) { - uint128 diff = uint128(block.number) - lastUpdate.blockNumber; - dataForToken[firstToken] = TokenData({ - reserve: newFirstTokenReserve, - accumulator: diff * oldFirstTokenData.reserve + oldFirstTokenData.accumulator - }); - dataForToken[secondToken] = TokenData({ - reserve: newSecondTokenReserve, - accumulator: diff * oldSecondTokenData.reserve + oldSecondTokenData.accumulator - }); - if (diff != 0) { - lastUpdate = LastUpdate({ - blockNumber: uint128(block.number), - time: uint128(block.timestamp) - }); + function updateData(uint256 balanceToken0, uint256 balanceToken1) private { + require(balanceToken0 <= uint128(-1) && balanceToken1 <= uint128(-1), "UniswapV2: OVERFLOW"); + + require(block.number <= uint64(-1), "UniswapV2: BLOCK_NUMBER_TOO_HIGH"); + uint64 blocksElapsed = uint64(block.number) - lastUpdate.blockNumber; + + // get token data + TokenData storage tokenDataToken0 = tokenData[token0]; + TokenData storage tokenDataToken1 = tokenData[token1]; + // TODO does this have a gas impact because it unnecessarily triggers for the 2nd+ trades within a block? + // update accumulators + tokenDataToken0.accumulator += tokenDataToken0.reserve * blocksElapsed; + tokenDataToken1.accumulator += tokenDataToken1.reserve * blocksElapsed; + // update reserves + tokenDataToken0.reserve = uint128(balanceToken0); + tokenDataToken1.reserve = uint128(balanceToken1); + + if (blocksElapsed > 0) { + require(block.timestamp <= uint64(-1), "UniswapV2: BLOCK_TIMESTAMP_TOO_HIGH"); + lastUpdate.blockNumber = uint64(block.number); + lastUpdate.blockTimestamp = uint64(block.timestamp); } } - - // TODO: consider switching to output token - function swap(address inputToken, address recipient) public nonReentrant returns (uint256) { - address outputToken; - if (inputToken == tokenA) { - outputToken = tokenB; - } else { - require(inputToken == tokenB, "Uniswap: INVALID_TOKEN"); - outputToken = tokenA; - } - - TokenData memory inputTokenData = dataForToken[inputToken]; - TokenData memory outputTokenData = dataForToken[outputToken]; - - uint256 newInputReserve = IERC20(inputToken).balanceOf(address(this)); - uint256 oldInputReserve = uint256(inputTokenData.reserve); - uint256 currentOutputReserve = IERC20(outputToken).balanceOf(address(this)); - uint256 amountSold = newInputReserve - oldInputReserve; - uint256 amountBought = getInputPrice(amountSold, oldInputReserve, currentOutputReserve); - require(IERC20(outputToken).transfer(recipient, amountBought), "Uniswap: TRANSFER_FAILED"); - uint256 newOutputReserve = currentOutputReserve - amountBought; - - updateData(inputToken, outputToken, inputTokenData, outputTokenData, uint128(newInputReserve), uint128(newOutputReserve)); - - emit Swap(inputToken, msg.sender, recipient, amountSold, amountBought); - - return amountBought; - } - - function addLiquidity(address recipient) public nonReentrant returns (uint256) { - uint256 _totalSupply = totalSupply; - - address _tokenA = tokenA; - address _tokenB = tokenB; - - TokenData memory tokenAData = dataForToken[_tokenA]; - TokenData memory tokenBData = dataForToken[_tokenB]; - - uint256 newReserveA = IERC20(_tokenA).balanceOf(address(this)); - uint256 newReserveB = IERC20(_tokenB).balanceOf(address(this)); - - uint256 amountA = newReserveA - tokenAData.reserve; - uint256 amountB = newReserveB - tokenBData.reserve; - - uint256 liquidityMinted; - - if (_totalSupply > 0) { - // TODO think about "donating" the non-min token amount by not updating stored balances - // TODO think about rounding here - liquidityMinted = Math.min(amountA.mul(_totalSupply).div(tokenAData.reserve), amountB.mul(_totalSupply).div(tokenBData.reserve)); - } else { - // TODO think through this (enforce min amount?) - liquidityMinted = amountA.mul(amountB).sqrt(); - } - - balanceOf[recipient] = balanceOf[recipient].add(liquidityMinted); - totalSupply = _totalSupply.add(liquidityMinted); - - updateData(_tokenA, _tokenB, tokenAData, tokenBData, uint128(newReserveA), uint128(newReserveB)); - - emit AddLiquidity(msg.sender, amountA, amountB); - emit Transfer(address(0), msg.sender, liquidityMinted); - - return liquidityMinted; - } - - function removeLiquidity(uint256 amount, address recipient) public nonReentrant returns (uint256, uint256) { - address _tokenA = tokenA; - address _tokenB = tokenB; - - TokenData memory tokenAData = dataForToken[_tokenA]; - TokenData memory tokenBData = dataForToken[_tokenB]; - - uint256 reserveA = IERC20(_tokenA).balanceOf(address(this)); - uint256 reserveB = IERC20(_tokenB).balanceOf(address(this)); - uint256 _totalSupply = totalSupply; - uint256 amountA = amount.mul(reserveA) / _totalSupply; - uint256 amountB = amount.mul(reserveB) / _totalSupply; - balanceOf[msg.sender] = balanceOf[msg.sender].sub(amount); - totalSupply = _totalSupply.sub(amount); - require(IERC20(_tokenA).transfer(recipient, amountA), "Uniswap: TRANSFER_A_FAILED"); - require(IERC20(_tokenB).transfer(recipient, amountB), "Uniswap: TRANSFER_B_FAILED"); - - updateData(_tokenA, _tokenB, tokenAData, tokenBData, uint128(reserveA - amountA), uint128(reserveB - amountB)); - - emit RemoveLiquidity(recipient, amountA, amountB); - emit Transfer(msg.sender, address(0), amount); - return (amountA, amountB); - } - // TODO merge/sync/donate function? think about the difference between over/under cases + + function getAmountOutput( + uint256 amountInput, + uint256 reserveInput, + uint256 reserveOutput + ) public pure returns (uint256 amountOutput) { + require(reserveInput > 0 && reserveOutput > 0, "UniswapV2: INVALID_VALUE"); + uint256 amountInputWithFee = amountInput.mul(997); + uint256 numerator = amountInputWithFee.mul(reserveOutput); + uint256 denominator = reserveInput.mul(1000).add(amountInputWithFee); + amountOutput = numerator.div(denominator); + } + + function mintLiquidity(address recipient) public lock returns (uint256 liquidity) { + // get balances + uint256 balanceToken0 = IERC20(token0).balanceOf(address(this)); + uint256 balanceToken1 = IERC20(token1).balanceOf(address(this)); + + // get reserves + uint256 reserveToken0 = uint256(tokenData[token0].reserve); + uint256 reserveToken1 = uint256(tokenData[token1].reserve); + + // get amounts + uint256 amountToken0 = balanceToken0.sub(reserveToken0); + uint256 amountToken1 = balanceToken1.sub(reserveToken1); + + if (totalSupply == 0) { + liquidity = amountToken0.mul(amountToken1).sqrt(); // TODO think through this (enforce min amount?) + } else { + // TODO think about "donating" the non-min token amount somehow + // TODO think about rounding here + liquidity = Math.min( + amountToken0.mul(totalSupply).div(reserveToken0), + amountToken1.mul(totalSupply).div(reserveToken1) + ); + } + + mint(recipient, liquidity); // TODO gas golf? + + updateData(balanceToken0, balanceToken1); + + emit LiquidityMinted(msg.sender, recipient, amountToken0, amountToken1); + } + + function burnLiquidity(uint256 amount, address recipient) public lock returns (uint256 amountToken0, uint256 amountToken1) { + require(amount > 0, "UniswapV2: ZERO_AMOUNT"); + + amountToken0 = amount.mul(tokenData[token0].reserve).div(totalSupply); + amountToken1 = amount.mul(tokenData[token1].reserve).div(totalSupply); + + burnFrom(msg.sender, amount); // TODO gas golf? + require(IERC20(token0).transfer(recipient, amountToken0), "UniswapV2: TRANSFER_FAILED"); + require(IERC20(token1).transfer(recipient, amountToken1), "UniswapV2: TRANSFER_FAILED"); + + // get balances + uint256 balanceToken0 = IERC20(token0).balanceOf(address(this)); + uint256 balanceToken1 = IERC20(token1).balanceOf(address(this)); + updateData(balanceToken0, balanceToken1); + + emit LiquidityBurned(msg.sender, recipient, amountToken0, amountToken1); + } + + function swap(address input, address recipient) public lock returns (uint256 amountOutput) { + require(input == token0 || input == token1, "UniswapV2: INVALID_INPUT"); + address output = input == token0 ? token1 : token0; + + // get balances + uint256 balanceInput = IERC20(input).balanceOf(address(this)); + + // get reserves + uint256 reserveInput = uint256(tokenData[input].reserve); + uint256 reserveOutput = uint256(tokenData[output].reserve); + + // get input amount + uint256 amountInput = balanceInput.sub(reserveInput); // TODO think through edge cases here + require(amountInput > 0, "UniswapV2: ZERO_AMOUNT"); + + // calculate output amount and send to the recipient + amountOutput = getAmountOutput(amountInput, reserveInput, reserveOutput); + require(IERC20(output).transfer(recipient, amountOutput), "UniswapV2: TRANSFER_FAILED"); // TODO fallback here + + // update data + uint256 balanceOutput = IERC20(output).balanceOf(address(this)); + input == token0 ? updateData(balanceInput, balanceOutput) : updateData(balanceOutput, balanceInput); + + emit Swap(input, msg.sender, recipient, amountInput, amountOutput); + } } diff --git a/contracts/UniswapV2Factory.sol b/contracts/UniswapV2Factory.sol index 33f67bc..bdd488a 100644 --- a/contracts/UniswapV2Factory.sol +++ b/contracts/UniswapV2Factory.sol @@ -3,6 +3,8 @@ pragma solidity 0.5.12; import "./interfaces/IUniswapV2Factory.sol"; +import "./UniswapV2.sol"; + contract UniswapV2Factory is IUniswapV2Factory { event ExchangeCreated(address indexed token0, address indexed token1, address exchange, uint256 exchangeCount); @@ -22,7 +24,7 @@ contract UniswapV2Factory is IUniswapV2Factory { } function orderTokens(address tokenA, address tokenB) private pure returns (Pair memory pair) { - pair = tokenA < tokenB ? Pair(tokenA, tokenB) : Pair(tokenB, tokenA); + pair = tokenA < tokenB ? Pair({ token0: tokenA, token1: tokenB }) : Pair({ token0: tokenB, token1: tokenA }); } function getTokens(address exchange) public view returns (address token0, address token1) { @@ -54,6 +56,7 @@ contract UniswapV2Factory is IUniswapV2Factory { salt ) } + UniswapV2(exchange).initialize(pair.token0, pair.token1); exchangeToPair[exchange] = pair; token0ToToken1ToExchange[pair.token0][pair.token1] = exchange; diff --git a/contracts/UniswapV2Helper.sol b/contracts/UniswapV2Helper.sol.unreviewed similarity index 99% rename from contracts/UniswapV2Helper.sol rename to contracts/UniswapV2Helper.sol.unreviewed index 996dfa5..da8431f 100644 --- a/contracts/UniswapV2Helper.sol +++ b/contracts/UniswapV2Helper.sol.unreviewed @@ -3,6 +3,7 @@ pragma solidity 0.5.12; import "./libraries/Math.sol"; import "./libraries/SafeMath.sol"; + import "./UniswapV2Factory.sol"; import "./UniswapV2.sol"; diff --git a/contracts/token/ERC20.sol b/contracts/token/ERC20.sol index cadf76c..06834a9 100644 --- a/contracts/token/ERC20.sol +++ b/contracts/token/ERC20.sol @@ -26,13 +26,19 @@ contract ERC20 is IERC20 { balanceOf[msg.sender] = totalSupply; } - function _transfer(address from, address to, uint256 value) internal { + function mint(address to, uint256 value) internal { + totalSupply = totalSupply.add(value); + balanceOf[to] = balanceOf[to].add(value); + emit Transfer(address(0), to, value); + } + + function _transfer(address from, address to, uint256 value) private { balanceOf[from] = balanceOf[from].sub(value); balanceOf[to] = balanceOf[to].add(value); emit Transfer(from, to, value); } - function _burn(address from, uint256 value) internal { + function _burn(address from, uint256 value) private { balanceOf[from] = balanceOf[from].sub(value); totalSupply = totalSupply.sub(value); emit Transfer(from, address(0), value); @@ -55,7 +61,7 @@ contract ERC20 is IERC20 { _burn(msg.sender, value); } - function burnFrom(address from, uint256 value) external { + function burnFrom(address from, uint256 value) public { if (allowance[from][msg.sender] != uint256(-1)) { allowance[from][msg.sender] = allowance[from][msg.sender].sub(value); }