diff --git a/contracts/UniswapV2.sol b/contracts/UniswapV2.sol index dbb1670..e43ece9 100644 --- a/contracts/UniswapV2.sol +++ b/contracts/UniswapV2.sol @@ -21,8 +21,7 @@ contract UniswapV2 is IUniswapV2, ERC20("Uniswap V2", "UNI-V2", 18, 0) { } struct TimeData { - uint64 blockNumber; - uint64 blockTimestamp; // overflows about 280 billion years after the earth's sun explodes + uint64 blockNumber; // overflows >280 billion years after the earth's sun explodes } bool private locked; // reentrancy lock @@ -87,11 +86,17 @@ contract UniswapV2 is IUniswapV2, ERC20("Uniswap V2", "UNI-V2", 18, 0) { } function getReservesCumulative() external view returns (uint128, uint128) { - return (reservesCumulative.token0, reservesCumulative.token1); - } - - function getLastUpdate() external view returns (uint64, uint64) { - return (lastUpdate.blockNumber, lastUpdate.blockTimestamp); + uint64 blockNumber = block.number.downcastTo64(); + if (blockNumber == lastUpdate.blockNumber) { + return (reservesCumulative.token0, reservesCumulative.token1); + } else { + // replicate the logic in updateReserves + // TODO do we want to make callers pay for our storage updates here instead of this view method? + uint64 blocksElapsed = blockNumber - lastUpdate.blockNumber; + uint128 reservesCumulativeToken0 = reservesCumulative.token0 + (reserves.token0 * blocksElapsed); + uint128 reservesCumulativeToken1 = reservesCumulative.token1 + (reserves.token1 * blocksElapsed); + return (reservesCumulativeToken0, reservesCumulativeToken1); + } } function getAmountOutput( @@ -118,7 +123,6 @@ contract UniswapV2 is IUniswapV2, ERC20("Uniswap V2", "UNI-V2", 18, 0) { // update last update lastUpdate.blockNumber = blockNumber; - lastUpdate.blockTimestamp = block.timestamp.downcastTo64(); } reserves.token0 = reservesNext.token0; @@ -164,7 +168,7 @@ contract UniswapV2 is IUniswapV2, ERC20("Uniswap V2", "UNI-V2", 18, 0) { function burnLiquidity(address recipient) external lock returns (uint128 amountToken0, uint128 amountToken1) { // get liquidity sent to be burned - uint256 liquidity = balanceOf[address(this)]; + uint256 liquidity = balanceOf[address(this)]; // TODO is this right? // require(liquidity > 0, "UniswapV2: ZERO_AMOUNT"); @@ -181,7 +185,7 @@ contract UniswapV2 is IUniswapV2, ERC20("Uniswap V2", "UNI-V2", 18, 0) { _burn(address(this), liquidity); - // TODO replace with reserves math? + // TODO replace with reserves math? TokenData memory balances = TokenData({ token0: IERC20(token0).balanceOf(address(this)).downcastTo128(), token1: IERC20(token1).balanceOf(address(this)).downcastTo128() diff --git a/contracts/UniswapV2Factory.sol b/contracts/UniswapV2Factory.sol index 4204509..3b89129 100644 --- a/contracts/UniswapV2Factory.sol +++ b/contracts/UniswapV2Factory.sol @@ -40,6 +40,10 @@ contract UniswapV2Factory is IUniswapV2Factory { return tokensToOtherTokens[token]; } + function getOtherTokensLength(address token) external view returns (uint256) { + return tokensToOtherTokens[token].length; + } + function getPair(address tokenA, address tokenB) private pure returns (Pair memory) { return tokenA < tokenB ? Pair({ token0: tokenA, token1: tokenB }) : Pair({ token0: tokenB, token1: tokenA }); } @@ -54,7 +58,8 @@ contract UniswapV2Factory is IUniswapV2Factory { bytes memory exchangeBytecodeMemory = exchangeBytecode; uint256 exchangeBytecodeLength = exchangeBytecode.length; - bytes32 salt = keccak256(abi.encodePacked(pair.token0, pair.token1, chainId)); + bytes32 salt = keccak256(abi.encodePacked(pair.token0, pair.token1, chainId)); // TODO include chainId? + // TODO constructor args? assembly { exchange := create2( 0, diff --git a/contracts/interfaces/IUniswapV2.sol b/contracts/interfaces/IUniswapV2.sol index f49418c..f6be4c8 100644 --- a/contracts/interfaces/IUniswapV2.sol +++ b/contracts/interfaces/IUniswapV2.sol @@ -17,7 +17,6 @@ interface IUniswapV2 { function getReserves() external view returns (uint128, uint128); function getReservesCumulative() external view returns (uint128, uint128); - function getLastUpdate() external view returns (uint64, uint64); function getAmountOutput( uint128 amountInput, uint128 reserveInput, uint128 reserveOutput diff --git a/contracts/interfaces/IUniswapV2Factory.sol b/contracts/interfaces/IUniswapV2Factory.sol index ad6eab6..d708866 100644 --- a/contracts/interfaces/IUniswapV2Factory.sol +++ b/contracts/interfaces/IUniswapV2Factory.sol @@ -10,6 +10,7 @@ interface IUniswapV2Factory { function getTokens(address exchange) external view returns (address, address); function getExchange(address tokenA, address tokenB) external view returns (address); function getOtherTokens(address token) external view returns (address[] memory); + function getOtherTokensLength(address token) external view returns (uint256); function createExchange(address tokenA, address tokenB) external returns (address exchange); } diff --git a/contracts/test/Oracle.sol b/contracts/test/Oracle.sol index d0326bd..fb2aed5 100644 --- a/contracts/test/Oracle.sol +++ b/contracts/test/Oracle.sol @@ -2,64 +2,85 @@ pragma solidity 0.5.12; import "../interfaces/IUniswapV2.sol"; +import "../libraries/Math.sol"; + contract Oracle { + using Math for uint256; + struct TokenData { uint128 token0; uint128 token1; } - struct Time { + struct TimeData { uint64 blockNumber; uint64 blockTimestamp; } address public exchange; - uint128 constant period = 1 days; + bool public initialized; + uint64 constant period = 1 days; TokenData private reservesCumulative; - Time private lastUpdate; + TimeData private lastUpdate; + TokenData private currentPrice; constructor(address _exchange) public { exchange = _exchange; } - function _updateCurrentPrice(TokenData memory averages, uint128 timestampDelta) private { + function _updateCurrentPrice(TokenData memory averages, uint64 timeElapsed) private { TokenData memory nextPrice; - if (timestampDelta >= period || (currentPrice.token0 == 0 && currentPrice.token1 == 0)) { + if (timeElapsed >= 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 + token0: (currentPrice.token0 * (period - timeElapsed) + averages.token0 * timeElapsed) / period, + token1: (currentPrice.token1 * (period - timeElapsed) + averages.token1 * timeElapsed) / 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(); + function initialize() external { + require(!initialized, "Oracle: ALREADY_INITIALIZED"); + IUniswapV2 uniswapV2 = IUniswapV2(exchange); + (uint128 reserveCumulativeToken0, uint128 reserveCumulativeToken1) = uniswapV2.getReservesCumulative(); + + reservesCumulative.token0 = reserveCumulativeToken0; + reservesCumulative.token1 = reserveCumulativeToken1; + lastUpdate.blockNumber = block.number.downcastTo64(); + lastUpdate.blockTimestamp = block.timestamp.downcastTo64(); + + initialized = true; + } + + function updateCurrentPrice() external { + require(initialized, "Oracle: UNINITIALIZED"); + + uint64 blockNumber = block.number.downcastTo64(); + // if we haven't updated this block yet... if (blockNumber > lastUpdate.blockNumber) { + IUniswapV2 uniswapV2 = IUniswapV2(exchange); + (uint128 reserveCumulativeToken0, uint128 reserveCumulativeToken1) = uniswapV2.getReservesCumulative(); + uint128 blocksElapsed = blockNumber - lastUpdate.blockNumber; - if (lastUpdate.blockNumber != 0) { - TokenData memory deltas = TokenData({ - token0: reserveCumulativeToken0 - reservesCumulative.token0, - token1: reserveCumulativeToken1 - reservesCumulative.token1 - }); + TokenData memory deltas = TokenData({ + token0: reserveCumulativeToken0 - reservesCumulative.token0, + token1: reserveCumulativeToken1 - reservesCumulative.token1 + }); - TokenData memory averages = TokenData({ - token0: deltas.token0 / blocksElapsed, - token1: deltas.token1 / blocksElapsed - }); + TokenData memory averages = TokenData({ + token0: deltas.token0 / blocksElapsed, + token1: deltas.token1 / blocksElapsed + }); - uint128 timeElapsed = blockTimestamp - lastUpdate.blockTimestamp; - _updateCurrentPrice(averages, timeElapsed); - } + uint64 blockTimestamp = block.timestamp.downcastTo64(); + uint64 timeElapsed = blockTimestamp - lastUpdate.blockTimestamp; + _updateCurrentPrice(averages, timeElapsed); reservesCumulative.token0 = reserveCumulativeToken0; reservesCumulative.token1 = reserveCumulativeToken1; diff --git a/contracts/token/ERC20.sol b/contracts/token/ERC20.sol index d7dbb11..c3689ef 100644 --- a/contracts/token/ERC20.sol +++ b/contracts/token/ERC20.sol @@ -19,7 +19,7 @@ contract ERC20 is IERC20 { // ERC-191 data uint256 public chainId; - mapping (address => uint) public nonceFor; + mapping (address => uint256) public nonceFor; event Transfer(address indexed from, address indexed to, uint256 value); event Approval(address indexed owner, address indexed spender, uint256 value); @@ -95,7 +95,7 @@ contract ERC20 is IERC20 { owner, spender, value, nonce, expiration, chainId )) )); - // TODO add ECDSA checks? https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/cryptography/ECDSA.sol + // TODO add ECDSA checks https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/cryptography/ECDSA.sol require(owner == ecrecover(digest, v, r, s), "ERC20: INVALID_SIGNATURE"); _approve(owner, spender, value); diff --git a/test/Oracle.spec.ts b/test/Oracle.spec.ts index 352a7c3..00608cc 100644 --- a/test/Oracle.spec.ts +++ b/test/Oracle.spec.ts @@ -52,7 +52,7 @@ describe('Oracle', () => { const token1Amount = expandTo18Decimals(5) await addLiquidity(token0Amount, token1Amount) - await oracle.connect(wallet).updateCurrentPrice() + await oracle.connect(wallet).initialize() await swap(token0, bigNumberify(1)) await oracle.connect(wallet).updateCurrentPrice() diff --git a/test/UniswapV2.spec.ts b/test/UniswapV2.spec.ts index 16037d5..c360202 100644 --- a/test/UniswapV2.spec.ts +++ b/test/UniswapV2.spec.ts @@ -109,10 +109,10 @@ describe('UniswapV2', () => { const token0Amount = expandTo18Decimals(3) const token1Amount = expandTo18Decimals(3) await addLiquidity(token0Amount, token1Amount) - const liquidity = expandTo18Decimals(3) + const liquidity = expandTo18Decimals(3) await exchange.connect(wallet).transfer(exchange.address, liquidity) - await expect(exchange.connect(wallet).burnLiquidity(liquidity, wallet.address)) + await expect(exchange.connect(wallet).burnLiquidity(wallet.address)) .to.emit(exchange, 'LiquidityBurned') .withArgs(wallet.address, wallet.address, liquidity, token0Amount, token1Amount) @@ -137,16 +137,14 @@ describe('UniswapV2', () => { expect(await exchange.getReserves()).to.deep.eq([token0Amount, token1Amount]) }) - it('getReservesCumulative, getLastUpdate', async () => { + it('getReservesCumulative', 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) await addLiquidity(token0Amount, token1Amount) 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) @@ -154,8 +152,6 @@ describe('UniswapV2', () => { await exchange.connect(wallet).swap(token0.address, wallet.address) 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]]) }) }) diff --git a/test/UniswapV2Factory.spec.ts b/test/UniswapV2Factory.spec.ts index 0ca717e..6b05c16 100644 --- a/test/UniswapV2Factory.spec.ts +++ b/test/UniswapV2Factory.spec.ts @@ -54,6 +54,8 @@ describe('UniswapV2Factory', () => { expect(await factory.getExchange(...tokens.slice().reverse())).to.eq(create2Address) expect(await factory.getOtherTokens(tokens[0])).to.deep.eq([tokens[1]]) expect(await factory.getOtherTokens(tokens[1])).to.deep.eq([tokens[0]]) + expect(await factory.getOtherTokensLength(tokens[0])).to.deep.eq(bigNumberify(1)) + expect(await factory.getOtherTokensLength(tokens[1])).to.deep.eq(bigNumberify(1)) const exchange = new Contract(create2Address, JSON.stringify(UniswapV2.abi), provider) expect(await exchange.factory()).to.eq(factory.address)