From 2c5a1afd39f22821eacf2ad1bd72d1f744f57da0 Mon Sep 17 00:00:00 2001 From: Noah Zinsmeister Date: Wed, 30 Oct 2019 18:56:12 -0400 Subject: [PATCH] add uint128 where applicable remove mapping in exchange add example oracle syntax changes --- contracts/UniswapV2.sol | 224 ++++++++---------- contracts/UniswapV2Factory.sol | 33 +-- contracts/interfaces/IUniswapV2.sol | 49 ++-- contracts/interfaces/IUniswapV2Factory.sol | 5 +- contracts/libraries/SafeMath128.sol | 29 +++ .../{SafeMath.sol => SafeMath256.sol} | 2 +- contracts/test/Oracle.sol | 74 ++++++ contracts/token/ERC20.sol | 4 +- test/Oracle.ts | 61 +++++ test/UniswapV2.ts | 34 ++- 10 files changed, 320 insertions(+), 195 deletions(-) create mode 100644 contracts/libraries/SafeMath128.sol rename contracts/libraries/{SafeMath.sol => SafeMath256.sol} (98%) create mode 100644 contracts/test/Oracle.sol create mode 100644 test/Oracle.ts diff --git a/contracts/UniswapV2.sol b/contracts/UniswapV2.sol index f4e9ae7..4a457e3 100644 --- a/contracts/UniswapV2.sol +++ b/contracts/UniswapV2.sol @@ -5,54 +5,44 @@ import "./interfaces/IERC20.sol"; import "./interfaces/IIncompatibleERC20.sol"; import "./libraries/Math.sol"; -import "./libraries/SafeMath.sol"; +import "./libraries/SafeMath128.sol"; +import "./libraries/SafeMath256.sol"; import "./token/ERC20.sol"; contract UniswapV2 is IUniswapV2, ERC20("Uniswap V2", "UNI-V2", 18, 0) { - using Math for uint256; - using SafeMath for uint256; + using SafeMath128 for uint128; + using SafeMath256 for uint256; - event Swap( - address indexed input, - address indexed sender, - address indexed recipient, - uint256 amountInput, - uint256 amountOutput - ); event LiquidityMinted( - address indexed sender, - address indexed recipient, - uint256 liquidity, - uint256 amountToken0, - uint256 amountToken1 + address indexed sender, address indexed recipient, uint256 liquidity, uint128 amountToken0, uint128 amountToken1 ); event LiquidityBurned( - address indexed sender, - address indexed recipient, - uint256 liquidity, - uint256 amountToken0, - uint256 amountToken1 + address indexed sender, address indexed recipient, uint256 liquidity, uint128 amountToken0, uint128 amountToken1 + ); + event Swap( + address indexed sender, address indexed recipient, address input, uint128 amountInput, uint128 amountOutput ); struct TokenData { - uint128 reserve; - uint128 accumulator; + uint128 token0; + uint128 token1; } - struct LastUpdate { + struct Time { uint64 blockNumber; uint64 blockTimestamp; // overflows about 280 billion years after the earth's sun explodes } - bool private locked; + bool private locked; // reentrancy lock address public factory; address public token0; address public token1; - mapping (address => TokenData) private tokenData; - LastUpdate private lastUpdate; + TokenData private reserves; + TokenData private reservesCumulative; + Time private lastUpdate; modifier lock() { require(!locked, "UniswapV2: LOCKED"); @@ -65,7 +55,7 @@ contract UniswapV2 is IUniswapV2, ERC20("Uniswap V2", "UNI-V2", 18, 0) { factory = msg.sender; } - function initialize(address _token0, address _token1, uint256 chainId) public { + function initialize(address _token0, address _token1, uint256 chainId) external { require(token0 == address(0) && token1 == address(0), "UniswapV2: ALREADY_INITIALIZED"); token0 = _token0; token1 = _token1; @@ -73,57 +63,47 @@ contract UniswapV2 is IUniswapV2, ERC20("Uniswap V2", "UNI-V2", 18, 0) { } // https://medium.com/coinmonks/missing-return-value-bug-at-least-130-tokens-affected-d67bf08521ca - function safeTransfer(address token, address to, uint value) private returns (bool result) { - IIncompatibleERC20(token).transfer(to, value); - + function safeTransfer(address token, address to, uint128 value) private returns (bool result) { + IIncompatibleERC20(token).transfer(to, uint256(value)); assembly { switch returndatasize() case 0 { - result := not(0) + result := not(0) // for no-bool responses, treat as successful } case 32 { returndatacopy(0, 0, 32) - result := mload(0) + result := mload(0) // for (presumably) bool responses, return whatever the function returned } default { - revert(0, 0) + revert(0, 0) // for invalid responses, revert } } } - // TODO merge/sync/donate function? think about the difference between over/under cases + // TODO sync/merge/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 getReserves() external view returns (uint128, uint128) { + return (reserves.token0, reserves.token1); } - 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 getReservesCumulative() external view returns (uint128, uint128) { + return (reservesCumulative.token0, reservesCumulative.token1); } - function updateData(uint256 reserveToken0, uint256 reserveToken1) private { + function getLastUpdate() external view returns (uint64, uint64) { + return (lastUpdate.blockNumber, lastUpdate.blockTimestamp); + } + + function updateReserves(TokenData memory reservesNext) private { uint64 blockNumber = block.number.downcastTo64(); uint64 blocksElapsed = blockNumber - lastUpdate.blockNumber; - // get token data - TokenData storage tokenDataToken0 = tokenData[token0]; - TokenData storage tokenDataToken1 = tokenData[token1]; - if (blocksElapsed > 0) { - // TODO do edge case math here - // update accumulators if this isn't the first call to updateData + // if this isn't the first-ever call to this function, update the accumulators if (lastUpdate.blockNumber != 0) { - tokenDataToken0.accumulator += tokenDataToken0.reserve * blocksElapsed; - tokenDataToken1.accumulator += tokenDataToken1.reserve * blocksElapsed; + // TODO do edge case math here + reservesCumulative.token0 += reserves.token0 * blocksElapsed; + reservesCumulative.token1 += reserves.token1 * blocksElapsed; } // update last update @@ -131,100 +111,102 @@ contract UniswapV2 is IUniswapV2, ERC20("Uniswap V2", "UNI-V2", 18, 0) { lastUpdate.blockTimestamp = block.timestamp.downcastTo64(); } - tokenDataToken0.reserve = reserveToken0.downcastTo128(); - tokenDataToken1.reserve = reserveToken1.downcastTo128(); + reserves.token0 = reservesNext.token0; + reserves.token1 = reservesNext.token1; } function getAmountOutput( - uint256 amountInput, - uint256 reserveInput, - uint256 reserveOutput - ) public pure returns (uint256 amountOutput) { + uint128 amountInput, uint128 reserveInput, uint128 reserveOutput + ) public pure returns (uint128 amountOutput) { 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); + uint256 amountInputWithFee = uint256(amountInput).mul(1000 - 3); // 30 bips for now, TODO think through this later + uint256 numerator = amountInputWithFee.mul(uint256(reserveOutput)); + uint256 denominator = uint256(reserveInput).mul(1000).add(amountInputWithFee); + amountOutput = numerator.div(denominator).downcastTo128(); } - function mintLiquidity(address recipient) public lock returns (uint256 liquidity) { + function mintLiquidity(address recipient) external lock returns (uint256 liquidity) { // get balances - uint256 balanceToken0 = IERC20(token0).balanceOf(address(this)); - uint256 balanceToken1 = IERC20(token1).balanceOf(address(this)); + TokenData memory balances = TokenData({ + token0: IERC20(token0).balanceOf(address(this)).downcastTo128(), + token1: IERC20(token1).balanceOf(address(this)).downcastTo128() + }); - // get reserves - uint256 reserveToken0 = uint256(tokenData[token0].reserve); - uint256 reserveToken1 = uint256(tokenData[token1].reserve); - - // TODO think about what happens if this fails - // get amounts - uint256 amountToken0 = balanceToken0.sub(reserveToken0); - uint256 amountToken1 = balanceToken1.sub(reserveToken1); + // get amounts sent to be added as liquidity + TokenData memory amounts = TokenData({ + token0: balances.token0.sub(reserves.token0), + token1: balances.token1.sub(reserves.token1) + }); if (totalSupply == 0) { - liquidity = amountToken0.mul(amountToken1).sqrt(); // TODO think through this (enforce min amount?) + // TODO is this right? enforce min amount? enforce no remainder? + liquidity = Math.sqrt(uint256(amounts.token0).mul(uint256(amounts.token1))); } else { - // TODO think about "donating" the non-min token amount somehow - // TODO think about rounding here + // TODO is this right? + // TODO "donate" or ignore the non-min token amount? + // TODO does this round the way we want? liquidity = Math.min( - amountToken0.mul(totalSupply).div(reserveToken0), - amountToken1.mul(totalSupply).div(reserveToken1) + uint256(amounts.token0).mul(totalSupply).div(uint256(reserves.token0)), + uint256(amounts.token1).mul(totalSupply).div(uint256(reserves.token1)) ); } mint(recipient, liquidity); // TODO gas golf? - - updateData(balanceToken0, balanceToken1); - - emit LiquidityMinted(msg.sender, recipient, liquidity, amountToken0, amountToken1); + updateReserves(balances); + emit LiquidityMinted(msg.sender, recipient, liquidity, amounts.token0, amounts.token1); } function burnLiquidity( - uint256 liquidity, - address recipient - ) public lock returns (uint256 amountToken0, uint256 amountToken1) { + uint256 liquidity, address recipient + ) external lock returns (uint128 amountToken0, uint128 amountToken1) { require(liquidity > 0, "UniswapV2: ZERO_AMOUNT"); - amountToken0 = liquidity.mul(tokenData[token0].reserve).div(totalSupply); - amountToken1 = liquidity.mul(tokenData[token1].reserve).div(totalSupply); + // send tokens back + // TODO is this right? + TokenData memory amounts = TokenData({ + token0: liquidity.mul(uint256(reserves.token0)).div(totalSupply).downcastTo128(), + token1: liquidity.mul(uint256(reserves.token1)).div(totalSupply).downcastTo128() + }); + (amountToken0, amountToken1) = (amounts.token0, amounts.token1); + require(safeTransfer(token0, recipient, amounts.token0), "UniswapV2: TRANSFER_FAILED"); + require(safeTransfer(token1, recipient, amounts.token1), "UniswapV2: TRANSFER_FAILED"); _burn(address(this), liquidity); // TODO gas golf? - require(safeTransfer(token0, recipient, amountToken0), "UniswapV2: TRANSFER_FAILED"); - require(safeTransfer(token1, recipient, amountToken1), "UniswapV2: TRANSFER_FAILED"); - - // get balances - uint256 balanceToken0 = IERC20(token0).balanceOf(address(this)); - uint256 balanceToken1 = IERC20(token1).balanceOf(address(this)); - updateData(balanceToken0, balanceToken1); + TokenData memory balances = TokenData({ + token0: IERC20(token0).balanceOf(address(this)).downcastTo128(), + token1: IERC20(token1).balanceOf(address(this)).downcastTo128() + }); + updateReserves(balances); emit LiquidityBurned(msg.sender, recipient, liquidity, 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; + function swap(address input, address recipient) external lock returns (uint128 amountOutput) { + uint128 inputBalance = IERC20(input).balanceOf(address(this)).downcastTo128(); - // get input balance - uint256 balanceInput = IERC20(input).balanceOf(address(this)); + uint128 amountInput; + TokenData memory balances; + if (input == token0) { + amountInput = inputBalance.sub(reserves.token0); + amountOutput = getAmountOutput(amountInput, reserves.token0, reserves.token1); + require(safeTransfer(token1, recipient, amountOutput), "UniswapV2: TRANSFER_FAILED"); + balances = TokenData({ + token0: inputBalance, + token1: IERC20(token1).balanceOf(address(this)).downcastTo128() + }); + } else { + require(input == token1, "UniswapV2: INVALID_INPUT"); - // get reserves - uint256 reserveInput = uint256(tokenData[input].reserve); - uint256 reserveOutput = uint256(tokenData[output].reserve); + amountInput = inputBalance.sub(reserves.token1); + amountOutput = getAmountOutput(amountInput, reserves.token1, reserves.token0); + require(safeTransfer(token0, recipient, amountOutput), "UniswapV2: TRANSFER_FAILED"); + balances = TokenData({ + token0: IERC20(token0).balanceOf(address(this)).downcastTo128(), + token1: inputBalance + }); + } - // TODO think about what happens if this fails - // get input amount - uint256 amountInput = balanceInput.sub(reserveInput); - - // calculate output amount and send to the recipient - amountOutput = getAmountOutput(amountInput, reserveInput, reserveOutput); - require(safeTransfer(output, recipient, amountOutput), "UniswapV2: TRANSFER_FAILED"); - - // update data - // TODO re-fetch input balance here? - uint256 balanceOutput = IERC20(output).balanceOf(address(this)); - input == token0 ? updateData(balanceInput, balanceOutput) : updateData(balanceOutput, balanceInput); - - emit Swap(input, msg.sender, recipient, amountInput, amountOutput); + updateReserves(balances); + emit Swap(msg.sender, recipient, input, amountInput, amountOutput); } } diff --git a/contracts/UniswapV2Factory.sol b/contracts/UniswapV2Factory.sol index cb7ebe1..d71116f 100644 --- a/contracts/UniswapV2Factory.sol +++ b/contracts/UniswapV2Factory.sol @@ -15,6 +15,7 @@ contract UniswapV2Factory is IUniswapV2Factory { bytes public exchangeBytecode; uint256 public chainId; uint256 public exchangeCount; + mapping (address => Pair) private exchangeToPair; mapping (address => mapping(address => address)) private token0ToToken1ToExchange; @@ -24,25 +25,15 @@ contract UniswapV2Factory is IUniswapV2Factory { chainId = _chainId; } - function orderTokens(address tokenA, address tokenB) private pure returns (Pair memory pair) { - pair = tokenA < tokenB ? Pair({ token0: tokenA, token1: tokenB }) : Pair({ token0: tokenB, token1: tokenA }); + function getPair(address tokenA, address tokenB) private pure returns (Pair memory) { + return tokenA < tokenB ? Pair({ token0: tokenA, token1: tokenB }) : Pair({ token0: tokenB, token1: tokenA }); } - function getTokens(address exchange) public view returns (address token0, address token1) { - Pair storage pair = exchangeToPair[exchange]; - (token0, token1) = (pair.token0, pair.token1); - } + function createExchange(address tokenA, address tokenB) external returns (address exchange) { + require(tokenA != tokenB, "UniswapV2Factory: SAME_ADDRESS"); + require(tokenA != address(0) && tokenB != address(0), "UniswapV2Factory: ZERO_ADDRESS"); - function getExchange(address tokenA, address tokenB) public view returns (address exchange) { - Pair memory pair = orderTokens(tokenA, tokenB); - exchange = token0ToToken1ToExchange[pair.token0][pair.token1]; - } - - function createExchange(address tokenA, address tokenB) public returns (address exchange) { - require(tokenA != tokenB, "UniswapV2Factory: SAME_TOKEN"); - require(tokenA != address(0) && tokenB != address(0), "UniswapV2Factory: ZERO_ADDRESS_TOKEN"); - - Pair memory pair = orderTokens(tokenA, tokenB); + Pair memory pair = getPair(tokenA, tokenB); require(token0ToToken1ToExchange[pair.token0][pair.token1] == address(0), "UniswapV2Factory: EXCHANGE_EXISTS"); @@ -63,4 +54,14 @@ contract UniswapV2Factory is IUniswapV2Factory { emit ExchangeCreated(pair.token0, pair.token1, exchange, exchangeCount++); } + + function getTokens(address exchange) external view returns (address, address) { + Pair storage pair = exchangeToPair[exchange]; + return (pair.token0, pair.token1); + } + + function getExchange(address tokenA, address tokenB) external view returns (address) { + Pair memory pair = getPair(tokenA, tokenB); + return token0ToToken1ToExchange[pair.token0][pair.token1]; + } } diff --git a/contracts/interfaces/IUniswapV2.sol b/contracts/interfaces/IUniswapV2.sol index b2e1cc2..e008737 100644 --- a/contracts/interfaces/IUniswapV2.sol +++ b/contracts/interfaces/IUniswapV2.sol @@ -1,51 +1,30 @@ pragma solidity 0.5.12; interface IUniswapV2 { - event Swap( - address indexed input, - address indexed sender, - address indexed recipient, - uint256 amountInput, - uint256 amountOutput - ); event LiquidityMinted( - address indexed sender, - address indexed recipient, - uint256 liquidity, - uint256 amountToken0, - uint256 amountToken1 + address indexed sender, address indexed recipient, uint256 liquidity, uint128 amountToken0, uint128 amountToken1 ); event LiquidityBurned( - address indexed sender, - address indexed recipient, - uint256 liquidity, - uint256 amountToken0, - uint256 amountToken1 + address indexed sender, address indexed recipient, uint256 liquidity, uint128 amountToken0, uint128 amountToken1 + ); + event Swap( + address indexed sender, address indexed recipient, address input, uint128 amountInput, uint128 amountOutput ); function factory() external view returns (address); 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 getReserves() external view returns (uint128, uint128); + function getReservesCumulative() external view returns (uint128, uint128); + function getLastUpdate() external view returns (uint64, uint64); + function getAmountOutput( - uint256 amountInput, - uint256 reserveInput, - uint256 reserveOutput - ) external pure returns (uint256 amountOutput); - - function initialize(address _token0, address _token1, uint256 chainId) external; - + uint128 amountInput, uint128 reserveInput, uint128 reserveOutput + ) external pure returns (uint128 amountOutput); function mintLiquidity(address recipient) external returns (uint256 liquidity); function burnLiquidity( - uint256 liquidity, - address recipient - ) external returns (uint256 amountToken0, uint256 amountToken1); - function swap(address input, address recipient) external returns (uint256 amountOutput); + uint256 liquidity, address recipient + ) external returns (uint128 amountToken0, uint128 amountToken1); + function swap(address input, address recipient) external returns (uint128 amountOutput); } diff --git a/contracts/interfaces/IUniswapV2Factory.sol b/contracts/interfaces/IUniswapV2Factory.sol index e1c769b..814950c 100644 --- a/contracts/interfaces/IUniswapV2Factory.sol +++ b/contracts/interfaces/IUniswapV2Factory.sol @@ -6,8 +6,9 @@ interface IUniswapV2Factory { function exchangeBytecode() external view returns (bytes memory); function chainId() external view returns (uint256); function exchangeCount() external view returns (uint256); - function getTokens(address exchange) external view returns (address token0, address token1); - function getExchange(address tokenA, address tokenB) external view returns (address exchange); function createExchange(address tokenA, address tokenB) external returns (address exchange); + + function getTokens(address exchange) external view returns (address, address); + function getExchange(address tokenA, address tokenB) external view returns (address); } diff --git a/contracts/libraries/SafeMath128.sol b/contracts/libraries/SafeMath128.sol new file mode 100644 index 0000000..cff1e89 --- /dev/null +++ b/contracts/libraries/SafeMath128.sol @@ -0,0 +1,29 @@ +// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/2f9ae975c8bdc5c7f7fa26204896f6c717f07164/contracts/math/SafeMath.sol +pragma solidity 0.5.12; + +library SafeMath128 { + function add(uint128 a, uint128 b) internal pure returns (uint128) { + uint128 c = a + b; + require(c >= a, "SafeMath: addition overflow"); + return c; + } + + function sub(uint128 a, uint128 b) internal pure returns (uint128) { + require(b <= a, "SafeMath: subtraction overflow"); + uint128 c = a - b; + return c; + } + + function mul(uint128 a, uint128 b) internal pure returns (uint128) { + if (a == 0) return 0; + uint128 c = a * b; + require(c / a == b, "SafeMath: multiplication overflow"); + return c; + } + + function div(uint128 a, uint128 b) internal pure returns (uint128) { + require(b > 0, "SafeMath: division by zero"); + uint128 c = a / b; + return c; + } +} diff --git a/contracts/libraries/SafeMath.sol b/contracts/libraries/SafeMath256.sol similarity index 98% rename from contracts/libraries/SafeMath.sol rename to contracts/libraries/SafeMath256.sol index 0280894..b2ebed0 100644 --- a/contracts/libraries/SafeMath.sol +++ b/contracts/libraries/SafeMath256.sol @@ -1,7 +1,7 @@ // https://github.com/OpenZeppelin/openzeppelin-contracts/blob/2f9ae975c8bdc5c7f7fa26204896f6c717f07164/contracts/math/SafeMath.sol pragma solidity 0.5.12; -library SafeMath { +library SafeMath256 { function add(uint256 a, uint256 b) internal pure returns (uint256) { uint256 c = a + b; require(c >= a, "SafeMath: addition overflow"); diff --git a/contracts/test/Oracle.sol b/contracts/test/Oracle.sol new file mode 100644 index 0000000..295fc6f --- /dev/null +++ b/contracts/test/Oracle.sol @@ -0,0 +1,74 @@ +pragma solidity 0.5.12; + +import "../interfaces/IUniswapV2.sol"; + +contract Oracle { + struct TokenData { + uint128 token0; + uint128 token1; + } + + struct Time { + uint64 blockNumber; + uint64 blockTimestamp; + } + + address public exchange; + uint128 constant period = 1 days; + + TokenData private reservesCumulative; + Time private lastUpdate; + TokenData private currentPrice; + + constructor(address _exchange) public { + exchange = _exchange; + } + + function _updateCurrentPrice(TokenData memory averages, uint128 timestampDelta) private { + TokenData memory nextPrice; + if (timestampDelta >= period || (currentPrice.token0 == 0 && currentPrice.token1 == 0)) { + nextPrice = averages; + } else { + nextPrice = TokenData({ + token0: (currentPrice.token0 * (period - timestampDelta) + averages.token0 * timestampDelta) / period, + token1: (currentPrice.token1 * (period - timestampDelta) + averages.token1 * timestampDelta) / period + }); + } + currentPrice = nextPrice; + } + + function updateCurrentPrice() external { + IUniswapV2 uniswapV2 = IUniswapV2(exchange); + // TODO handle the case where time has passed (basically, always use the most up-to-date data) + (uint128 reserveCumulativeToken0, uint128 reserveCumulativeToken1) = uniswapV2.getReservesCumulative(); + (uint64 blockNumber, uint64 blockTimestamp) = uniswapV2.getLastUpdate(); + + if (blockNumber > lastUpdate.blockNumber) { + uint128 blocksElapsed = blockNumber - lastUpdate.blockNumber; + + if (lastUpdate.blockNumber != 0) { + TokenData memory deltas = TokenData({ + token0: reserveCumulativeToken0 - reservesCumulative.token0, + token1: reserveCumulativeToken1 - reservesCumulative.token1 + }); + + TokenData memory averages = TokenData({ + token0: deltas.token0 / blocksElapsed, + token1: deltas.token1 / blocksElapsed + }); + + uint128 timeElapsed = blockTimestamp - lastUpdate.blockTimestamp; + _updateCurrentPrice(averages, timeElapsed); + } + + reservesCumulative.token0 = reserveCumulativeToken0; + reservesCumulative.token1 = reserveCumulativeToken1; + lastUpdate.blockNumber = blockNumber; + lastUpdate.blockTimestamp = blockTimestamp; + } + } + + function getCurrentPrice() external view returns (uint128, uint128) { + return (currentPrice.token0, currentPrice.token1); + } +} diff --git a/contracts/token/ERC20.sol b/contracts/token/ERC20.sol index e8461bc..66cf90f 100644 --- a/contracts/token/ERC20.sol +++ b/contracts/token/ERC20.sol @@ -4,10 +4,10 @@ pragma solidity 0.5.12; import "../interfaces/IERC20.sol"; -import "../libraries/SafeMath.sol"; +import "../libraries/SafeMath256.sol"; contract ERC20 is IERC20 { - using SafeMath for uint256; + using SafeMath256 for uint256; // ERC-20 data string public name; diff --git a/test/Oracle.ts b/test/Oracle.ts new file mode 100644 index 0000000..352a7c3 --- /dev/null +++ b/test/Oracle.ts @@ -0,0 +1,61 @@ +import path from 'path' +import chai from 'chai' +import { solidity, createMockProvider, getWallets, createFixtureLoader, deployContract } from 'ethereum-waffle' +import { Contract } from 'ethers' +import { BigNumber, bigNumberify } from 'ethers/utils' + +import { expandTo18Decimals } from './shared/utilities' +import { exchangeFixture, ExchangeFixture } from './shared/fixtures' + +import Oracle from '../build/Oracle.json' + +chai.use(solidity) +const { expect } = chai + +describe('Oracle', () => { + const provider = createMockProvider(path.join(__dirname, '..', 'waffle.json')) + const [wallet] = getWallets(provider) + const loadFixture = createFixtureLoader(provider, [wallet]) + + let token0: Contract + let token1: Contract + let exchange: Contract + let oracle: Contract + beforeEach(async () => { + const { token0: _token0, token1: _token1, exchange: _exchange } = (await loadFixture( + exchangeFixture as any + )) as ExchangeFixture + token0 = _token0 + token1 = _token1 + exchange = _exchange + oracle = await deployContract(wallet, Oracle, [exchange.address]) + }) + + 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) + } + + async function swap(token: Contract, amount: BigNumber) { + await token.transfer(exchange.address, amount) + await exchange.connect(wallet).swap(token.address, wallet.address) + } + + it('exchange, getCurrentPrice', async () => { + expect(await oracle.exchange()).to.eq(exchange.address) + expect(await oracle.getCurrentPrice()).to.deep.eq([0, 0].map(n => bigNumberify(n))) + }) + + it('updateCurrentPrice', async () => { + const token0Amount = expandTo18Decimals(10) + const token1Amount = expandTo18Decimals(5) + + await addLiquidity(token0Amount, token1Amount) + await oracle.connect(wallet).updateCurrentPrice() + await swap(token0, bigNumberify(1)) + + await oracle.connect(wallet).updateCurrentPrice() + expect(await oracle.getCurrentPrice()).to.deep.eq([token0Amount, token1Amount]) + }) +}) diff --git a/test/UniswapV2.ts b/test/UniswapV2.ts index 66a7d35..16037d5 100644 --- a/test/UniswapV2.ts +++ b/test/UniswapV2.ts @@ -59,7 +59,7 @@ describe('UniswapV2', () => { '0996006981039903216' ].map((n: string) => bigNumberify(n)) - const outputs = await Promise.all(testCases.map(c => exchange.getAmountOutput(...c))) + const outputs = await Promise.all(testCases.map(a => exchange.getAmountOutput(...a))) expect(outputs).to.deep.eq(expectedOutputs) }) @@ -96,7 +96,7 @@ describe('UniswapV2', () => { 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) + .withArgs(wallet.address, wallet.address, token0.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)) @@ -128,36 +128,34 @@ describe('UniswapV2', () => { }) it('getReserves', async () => { + expect(await exchange.getReserves()).to.deep.eq([0, 0].map(n => bigNumberify(n))) + const token0Amount = expandTo18Decimals(3) const token1Amount = expandTo18Decimals(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 () => { + it('getReservesCumulative, getLastUpdate', async () => { + expect(await exchange.getReservesCumulative()).to.deep.eq([0, 0].map(n => bigNumberify(n))) + expect(await exchange.getLastUpdate()).to.deep.eq([0, 0].map(n => bigNumberify(n))) + const token0Amount = expandTo18Decimals(3) const token1Amount = expandTo18Decimals(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 reservesCumulativePre = await exchange.getReservesCumulative() + const lastUpdatePre = await exchange.getLastUpdate() + expect(reservesCumulativePre).to.deep.eq([0, 0].map(n => bigNumberify(n))) 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] - ]) + const reservesCumulativePost = await exchange.getReservesCumulative() + const lastUpdatePost = await exchange.getLastUpdate() + expect(reservesCumulativePost).to.deep.eq([token0Amount.mul(bigNumberify(2)), token1Amount.mul(bigNumberify(2))]) + expect(lastUpdatePost).to.deep.eq([lastUpdatePre[0].add(bigNumberify(2)), lastUpdatePost[1]]) }) })