diff --git a/README.md b/README.md index 87f7061..3dd7f2c 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ ## Local Development +The following assumes the use of `node@^10`. + ### Clone Repository ``` git clone https://github.com/Uniswap/uniswap-v2.git diff --git a/contracts/UniswapV2.sol b/contracts/UniswapV2.sol index 2186e6f..f4e9ae7 100644 --- a/contracts/UniswapV2.sol +++ b/contracts/UniswapV2.sol @@ -91,8 +91,27 @@ contract UniswapV2 is IUniswapV2, ERC20("Uniswap V2", "UNI-V2", 18, 0) { } } + // TODO merge/sync/donate function? think about the difference between over/under cases + + function getReserves() public view returns (uint128 reserveToken0, uint128 reserveToken1) { + reserveToken0 = tokenData[token0].reserve; + reserveToken1 = tokenData[token1].reserve; + } + + function getData() public view returns ( + uint128 accumulatorToken0, + uint128 accumulatorToken1, + uint64 blockNumber, + uint64 blockTimestamp + ) { + accumulatorToken0 = tokenData[token0].accumulator; + accumulatorToken1 = tokenData[token1].accumulator; + blockNumber = lastUpdate.blockNumber; + blockTimestamp = lastUpdate.blockTimestamp; + } + function updateData(uint256 reserveToken0, uint256 reserveToken1) private { - uint64 blockNumber = (block.number).downcastTo64(); + uint64 blockNumber = block.number.downcastTo64(); uint64 blocksElapsed = blockNumber - lastUpdate.blockNumber; // get token data @@ -101,28 +120,29 @@ contract UniswapV2 is IUniswapV2, ERC20("Uniswap V2", "UNI-V2", 18, 0) { if (blocksElapsed > 0) { // TODO do edge case math here - // update accumulators - tokenDataToken0.accumulator += tokenDataToken0.reserve * blocksElapsed; - tokenDataToken1.accumulator += tokenDataToken1.reserve * blocksElapsed; + // update accumulators if this isn't the first call to updateData + if (lastUpdate.blockNumber != 0) { + tokenDataToken0.accumulator += tokenDataToken0.reserve * blocksElapsed; + tokenDataToken1.accumulator += tokenDataToken1.reserve * blocksElapsed; + } // update last update lastUpdate.blockNumber = blockNumber; - lastUpdate.blockTimestamp = (block.timestamp).downcastTo64(); + lastUpdate.blockTimestamp = block.timestamp.downcastTo64(); } tokenDataToken0.reserve = reserveToken0.downcastTo128(); tokenDataToken1.reserve = reserveToken1.downcastTo128(); } - // 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); + require(amountInput > 0 && reserveInput > 0 && reserveOutput > 0, "UniswapV2: INVALID_VALUE"); + uint256 fee = 3; // TODO 30 bips for now, think through this later + uint256 amountInputWithFee = amountInput.mul(1000 - fee); uint256 numerator = amountInputWithFee.mul(reserveOutput); uint256 denominator = reserveInput.mul(1000).add(amountInputWithFee); amountOutput = numerator.div(denominator); @@ -169,7 +189,7 @@ contract UniswapV2 is IUniswapV2, ERC20("Uniswap V2", "UNI-V2", 18, 0) { amountToken0 = liquidity.mul(tokenData[token0].reserve).div(totalSupply); amountToken1 = liquidity.mul(tokenData[token1].reserve).div(totalSupply); - burn(liquidity); // TODO gas golf? + _burn(address(this), liquidity); // TODO gas golf? require(safeTransfer(token0, recipient, amountToken0), "UniswapV2: TRANSFER_FAILED"); require(safeTransfer(token1, recipient, amountToken1), "UniswapV2: TRANSFER_FAILED"); @@ -195,7 +215,6 @@ contract UniswapV2 is IUniswapV2, ERC20("Uniswap V2", "UNI-V2", 18, 0) { // TODO think about what happens if this fails // get input amount uint256 amountInput = balanceInput.sub(reserveInput); - require(amountInput > 0, "UniswapV2: ZERO_AMOUNT"); // calculate output amount and send to the recipient amountOutput = getAmountOutput(amountInput, reserveInput, reserveOutput); diff --git a/contracts/interfaces/IUniswapV2.sol b/contracts/interfaces/IUniswapV2.sol index 31f579a..b2e1cc2 100644 --- a/contracts/interfaces/IUniswapV2.sol +++ b/contracts/interfaces/IUniswapV2.sol @@ -27,6 +27,13 @@ interface IUniswapV2 { function token0() external view returns (address); function token1() external view returns (address); + function getReserves() external view returns (uint128 reserveToken0, uint128 reserveToken1); + function getData() external view returns ( + uint128 accumulatorToken0, + uint128 accumulatorToken1, + uint64 blockNumber, + uint64 blockTimestamp + ); function getAmountOutput( uint256 amountInput, uint256 reserveInput, diff --git a/contracts/token/ERC20.sol b/contracts/token/ERC20.sol index 4301a63..e8461bc 100644 --- a/contracts/token/ERC20.sol +++ b/contracts/token/ERC20.sol @@ -9,6 +9,7 @@ import "../libraries/SafeMath.sol"; contract ERC20 is IERC20 { using SafeMath for uint256; + // ERC-20 data string public name; string public symbol; uint8 public decimals; @@ -16,7 +17,7 @@ contract ERC20 is IERC20 { mapping (address => uint256) public balanceOf; mapping (address => mapping (address => uint256)) public allowance; - // EIP-191 + // ERC-191 data uint256 public chainId; mapping (address => uint) public nonceFor; @@ -34,7 +35,7 @@ contract ERC20 is IERC20 { require(chainId == 0, "ERC20: ALREADY_INITIALIZED"); chainId = _chainId; } - + function mint(address to, uint256 value) internal { totalSupply = totalSupply.add(value); balanceOf[to] = balanceOf[to].add(value); @@ -47,7 +48,7 @@ contract ERC20 is IERC20 { emit Transfer(from, to, value); } - function _burn(address from, uint256 value) private { + function _burn(address from, uint256 value) internal { balanceOf[from] = balanceOf[from].sub(value); totalSupply = totalSupply.sub(value); emit Transfer(from, address(0), value); @@ -71,7 +72,7 @@ contract ERC20 is IERC20 { return true; } - function burn(uint256 value) public { + function burn(uint256 value) external { _burn(msg.sender, value); } diff --git a/test/ERC20.ts b/test/ERC20.ts index 0afe553..a4d7e70 100644 --- a/test/ERC20.ts +++ b/test/ERC20.ts @@ -13,7 +13,7 @@ const { expect } = chai const decimalize = (n: number): BigNumber => bigNumberify(n).mul(bigNumberify(10).pow(18)) -const name = 'Mock ERC20' +const name = 'Mock Token' const symbol = 'MOCK' const decimals = 18 diff --git a/test/UniswapV2.ts b/test/UniswapV2.ts index 3ed42f0..5cdb4cb 100644 --- a/test/UniswapV2.ts +++ b/test/UniswapV2.ts @@ -13,10 +13,10 @@ const { expect } = chai const chainId = 1 -const decimalize = (n: number): BigNumber => bigNumberify(n).mul(bigNumberify(10).pow(18)) +const decimalize = (n: number | string): BigNumber => bigNumberify(n).mul(bigNumberify(10).pow(18)) -const token0Details = ['Token 0', 'T0', 18, decimalize(100), chainId] -const token1Details = ['Token 1', 'T1', 18, decimalize(100), chainId] +const tokenADetails = ['Mock Token A', 'MOCKA', 18, decimalize(100), chainId] +const tokenBDetails = ['Mock Token B', 'MOCKB', 18, decimalize(100), chainId] describe('UniswapV2', () => { const provider = createMockProvider(path.join(__dirname, '..', 'waffle.json')) @@ -27,8 +27,11 @@ describe('UniswapV2', () => { let exchange: Contract beforeEach(async () => { - token0 = await deployContract(wallet, ERC20, token0Details) - token1 = await deployContract(wallet, ERC20, token1Details) + const tokenA = await deployContract(wallet, ERC20, tokenADetails) + const tokenB = await deployContract(wallet, ERC20, tokenBDetails) + + token0 = tokenA.address < tokenB.address ? tokenA : tokenB + token1 = tokenA.address < tokenB.address ? tokenB : tokenA const bytecode = `0x${UniswapV2.evm.bytecode.object}` factory = await deployContract(wallet, UniswapV2Factory, [bytecode, chainId], { @@ -40,11 +43,133 @@ describe('UniswapV2', () => { exchange = new Contract(exchangeAddress, UniswapV2.abi, provider) }) + it('initialize:fail', async () => { + await expect(exchange.connect(wallet).initialize(token0.address, token1.address, chainId)).to.be.revertedWith( + 'UniswapV2: ALREADY_INITIALIZED' + ) + }) + + it('getAmountOutput', async () => { + const testCases: BigNumber[][] = [ + ['1', '005', '010'].map((n: string) => decimalize(n)), + ['1', '010', '005'].map((n: string) => decimalize(n)), + + ['2', '005', '010'].map((n: string) => decimalize(n)), + ['2', '010', '005'].map((n: string) => decimalize(n)), + + ['1', '010', '010'].map((n: string) => decimalize(n)), + ['1', '100', '100'].map((n: string) => decimalize(n)) + ] + + const expectedOutputs: BigNumber[] = [ + '1662497915624478906', + '0453305446940074565', + + '2851015155847869602', + '0831248957812239453', + + '0906610893880149131', + '0987158034397061298' + ].map((n: string) => bigNumberify(n)) + + const outputs = await Promise.all(testCases.map(c => exchange.getAmountOutput(...c))) + expect(outputs).to.deep.eq(expectedOutputs) + }) + it('mintLiquidity', async () => { - await token0.transfer(exchange.address, decimalize(4)) - await token1.transfer(exchange.address, decimalize(1)) + const token0Amount = decimalize(1) + const token1Amount = decimalize(4) + const expectedLiquidity = decimalize(2) + + await token0.transfer(exchange.address, token0Amount) + await token1.transfer(exchange.address, token1Amount) await expect(exchange.connect(wallet).mintLiquidity(wallet.address)) .to.emit(exchange, 'LiquidityMinted') - .withArgs(wallet.address, wallet.address, decimalize(2), decimalize(4), decimalize(1)) + .withArgs(wallet.address, wallet.address, expectedLiquidity, token0Amount, token1Amount) + + expect(await exchange.totalSupply()).to.eq(expectedLiquidity) + expect(await exchange.balanceOf(wallet.address)).to.eq(expectedLiquidity) + }) + + async function addLiquidity(token0Amount: BigNumber, token1Amount: BigNumber) { + await token0.transfer(exchange.address, token0Amount) + await token1.transfer(exchange.address, token1Amount) + await exchange.connect(wallet).mintLiquidity(wallet.address) + } + + it('swap', async () => { + const token0Amount = decimalize(5) + const token1Amount = decimalize(10) + await addLiquidity(token0Amount, token1Amount) + + const swapAmount = decimalize(1) + const expectedOutputAmount = bigNumberify('1662497915624478906') + + await token0.transfer(exchange.address, swapAmount) + await expect(exchange.connect(wallet).swap(token0.address, wallet.address)) + .to.emit(exchange, 'Swap') + .withArgs(token0.address, wallet.address, wallet.address, swapAmount, expectedOutputAmount) + + expect(await token0.balanceOf(exchange.address)).to.eq(token0Amount.add(swapAmount)) + expect(await token1.balanceOf(exchange.address)).to.eq(token1Amount.sub(expectedOutputAmount)) + + const totalSupplyToken1 = await token1.totalSupply() + expect(await token1.balanceOf(wallet.address)).to.eq(totalSupplyToken1.sub(token1Amount).add(expectedOutputAmount)) + }) + + it('burnLiquidity', async () => { + const token0Amount = decimalize(3) + const token1Amount = decimalize(3) + await addLiquidity(token0Amount, token1Amount) + const liquidity = decimalize(3) + + await exchange.connect(wallet).transfer(exchange.address, liquidity) + await expect(exchange.connect(wallet).burnLiquidity(liquidity, wallet.address)) + .to.emit(exchange, 'LiquidityBurned') + .withArgs(wallet.address, wallet.address, liquidity, token0Amount, token1Amount) + + expect(await exchange.balanceOf(wallet.address)).to.eq(0) + expect(await token0.balanceOf(exchange.address)).to.eq(0) + expect(await token1.balanceOf(exchange.address)).to.eq(0) + + const totalSupplyToken0 = await token0.totalSupply() + const totalSupplyToken1 = await token1.totalSupply() + + expect(await token0.balanceOf(wallet.address)).to.eq(totalSupplyToken0) + expect(await token1.balanceOf(wallet.address)).to.eq(totalSupplyToken1) + }) + + it('getReserves', async () => { + const token0Amount = decimalize(3) + const token1Amount = decimalize(3) + + expect(await exchange.getReserves()).to.deep.eq([0, 0].map(n => bigNumberify(n))) + await addLiquidity(token0Amount, token1Amount) + expect(await exchange.getReserves()).to.deep.eq([token0Amount, token1Amount]) + }) + + it('getData', async () => { + const token0Amount = decimalize(3) + const token1Amount = decimalize(3) + + const preData = await exchange.getData() + expect(preData).to.deep.eq([0, 0, 0, 0].map(n => bigNumberify(n))) + + await addLiquidity(token0Amount, token1Amount) + + const data = await exchange.getData() + expect(data).to.deep.eq([0, 0].map(n => bigNumberify(n)).concat(data.slice(2, 4))) + + const dummySwapAmount = bigNumberify(1) + await token0.transfer(exchange.address, dummySwapAmount) + await exchange.connect(wallet).swap(token0.address, wallet.address) + + const postData = await exchange.getData() + expect(postData).to.deep.eq([ + token0Amount.mul(bigNumberify(2)), + token1Amount.mul(bigNumberify(2)), + data[2].add(bigNumberify(2)), + postData[3] + ]) }) }) diff --git a/test/UniswapV2Factory.ts b/test/UniswapV2Factory.ts index 843fc1a..6264139 100644 --- a/test/UniswapV2Factory.ts +++ b/test/UniswapV2Factory.ts @@ -38,6 +38,12 @@ describe('UniswapV2Factory', () => { }) }) + it('exchangeBytecode, chainId, exchangeCount', async () => { + expect(await factory.exchangeBytecode()).to.eq(bytecode) + expect(await factory.chainId()).to.eq(chainId) + expect(await factory.exchangeCount()).to.eq(0) + }) + async function createExchange(tokens: string[]) { const expectedAddress = getExpectedAddress(factory.address, bytecode) @@ -61,12 +67,6 @@ describe('UniswapV2Factory', () => { expect(await exchange.token1()).to.eq(dummyTokens[1]) } - it('exchangeBytecode, chainId, exchangeCount', async () => { - expect(await factory.exchangeBytecode()).to.eq(bytecode) - expect(await factory.chainId()).to.eq(chainId) - expect(await factory.exchangeCount()).to.eq(0) - }) - it('createExchange', async () => { await createExchange(dummyTokens) })