Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Demo router with sync calls #21

Merged
merged 8 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,35 @@ as well as performing operations like minting, swapping, and burning
1. **Using Factory and Pair Contracts Only**
This demo handles deploying the Factory and Pair contracts and executing a complete flow of operations
[View the demo task](https://github.com/NilFoundation/uniswap-v2-nil/blob/main/tasks/core/demo.ts)

**Important:**
- Calculations are processed on the user's side.
- Both the currency address and its ID are stored in the pair contract.


2. **Using Factory, Pair, and Router Contracts**
This demo includes an additional layer by utilizing the Router contract along with Factory and Pair
[View the demo-router task](https://github.com/NilFoundation/uniswap-v2-nil/blob/main/tasks/core/demo-router.ts)

**Important:**
- The `UniswapV2Factory` is used for creating new pairs. `UniswapV2Router01` calls already deployed pair contracts.
- `UniswapV2Router01` can be deployed on a different shard.
- Vulnerability: no checks are performed during adding/removing liquidity and swaps.
Rates and output amounts are entirely calculated on the user side.


3. **Using Router with Sync Calls (1 shard)**
This demo task shows how to deploy the `UniswapV2Router01` contract
and use it as a proxy for adding/removing liquidity and swaps via sync calls.
It allows checks on amounts before pair calls and maintains currency rates.
[View the demo-router task](https://github.com/NilFoundation/uniswap-v2-nil/blob/main/tasks/core/demo-router-sync.ts)

**Important:**
- `UniswapV2Router01` should be deployed on the same shard as the pair contract.
- It maintains the currency exchange rate when adding/removing liquidity.
- It supports limit checks for currency amounts.


### Running the Demo Tasks
1. **Compile the Project**:
```bash
Expand All @@ -60,6 +84,10 @@ as well as performing operations like minting, swapping, and burning
```bash
npx hardhat demo-router --network nil
```
- For the demo with Router (Sync calls):
```bash
npx hardhat demo-router-sync --network nil
```

### Manual Setup
If you prefer to run everything manually, we provide Ignition modules for each contract:
Expand All @@ -80,4 +108,3 @@ Your input and contributions are greatly appreciated!

## License
This project is licensed under the GPL-3.0 License. See the [LICENSE](./LICENSE) file for more details. Portions of this project are derived from [Uniswap V2](https://github.com/Uniswap/v2-core) and are also subject to the GPL-3.0 License.

7 changes: 7 additions & 0 deletions contracts/UniswapV2Pair.sol
Original file line number Diff line number Diff line change
Expand Up @@ -235,5 +235,12 @@ contract UniswapV2Pair is NilCurrencyBase, IUniswapV2Pair {
);
}

function token0Id() external view returns (uint256) {
return tokenId0;
}
function token1Id() external view returns (uint256) {
return tokenId1;
}

receive() external payable {}
}
157 changes: 104 additions & 53 deletions contracts/UniswapV2Router01.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,10 @@ import "@nilfoundation/smart-contracts/contracts/Nil.sol";
import "./interfaces/IUniswapV2Pair.sol";

contract UniswapV2Router01 is IUniswapV2Router01, NilCurrencyBase {
address public immutable factory;


constructor(address _factory) public {
// Revert if the factory address is the zero address or an empty string
require(_factory != address(0), "Factory address cannot be the zero address");

factory = _factory;
modifier sameShard(address _addr) {
require(Nil.getShardId(_addr) == Nil.getShardId(address(this)), "Sync calls require same shard for all contracts");
_;
}

function addLiquidity(
Expand All @@ -30,6 +26,67 @@ contract UniswapV2Router01 is IUniswapV2Router01, NilCurrencyBase {
smartCall(pair, tokens, abi.encodeWithSignature("mint(address)", to));
}

function addLiquiditySync(
address pair,
address to,
uint amountADesired,
uint amountBDesired,
uint amountAMin,
uint amountBMin
) public override sameShard(pair) returns (uint amountA, uint amountB) {
Nil.Token[] memory tokens = Nil.msgTokens();
if (tokens.length != 2) {
revert("Send only 2 tokens to add liquidity");
}
(amountA, amountB) = _addLiquiditySync(pair, tokens[0].id, tokens[1].id, amountADesired, amountBDesired, amountAMin, amountBMin);

if (amountA < tokens[0].amount) {
Nil.Token[] memory tokenAReturns = new Nil.Token[](1);
tokenAReturns[0].id = tokens[0].id;
tokenAReturns[0].amount = tokens[0].amount - amountA;
smartCall(to, tokenAReturns, "");
}
if (amountB < tokens[1].amount) {
Nil.Token[] memory tokenBReturns = new Nil.Token[](1);
tokenBReturns[0].id = tokens[1].id;
tokenBReturns[0].amount = tokens[1].amount - amountB;
smartCall(to, tokenBReturns, "");
}

Nil.Token[] memory tokensToSend = new Nil.Token[](2);
tokensToSend[0].id = tokens[0].id;
tokensToSend[0].amount = amountA;
tokensToSend[1].id = tokens[1].id;
tokensToSend[1].amount = amountA;
smartCall(pair, tokensToSend, abi.encodeWithSignature("mint(address)", to));
}

function _addLiquiditySync(
address pair,
uint256 tokenA,
uint256 tokenB,
uint amountADesired,
uint amountBDesired,
uint amountAMin,
uint amountBMin
) private returns (uint amountA, uint amountB) {
(uint reserveA, uint reserveB) = UniswapV2Library.getReserves(pair, tokenA, tokenB);
if (reserveA == 0 && reserveB == 0) {
(amountA, amountB) = (amountADesired, amountBDesired);
} else {
uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB);
if (amountBOptimal <= amountBDesired) {
require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
(amountA, amountB) = (amountADesired, amountBOptimal);
} else {
uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA);
assert(amountAOptimal <= amountADesired);
require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
(amountA, amountB) = (amountAOptimal, amountBDesired);
}
}
}

// **** REMOVE LIQUIDITY ****
function removeLiquidity(
address pair,
Expand All @@ -42,59 +99,53 @@ contract UniswapV2Router01 is IUniswapV2Router01, NilCurrencyBase {
smartCall(pair, tokens, abi.encodeWithSignature("burn(address)", to));
}

function swap(address to, address pair, uint amount0Out, uint amount1Out) public override {
function removeLiquiditySync(
address pair,
address to,
uint amountAMin,
uint amountBMin
) public override sameShard(pair) returns (uint amountA, uint amountB) {
Nil.Token[] memory tokens = Nil.msgTokens();
if (tokens.length != 1) {
revert("UniswapV2Router: should contains only pair token");
}
smartCall(pair, tokens, abi.encodeWithSignature("swap(uint256,uint256,address)", amount0Out, amount1Out, to));
}

// TODO: This method are used for swapping via multiple pairs. Not supported in nil for now
// **** SWAP ****
// requires the initial amount to have already been sent to the first pair
function _swap(uint[] memory amounts, address[] memory path, address _to) private {
for (uint i; i < path.length - 1; i++) {
(address input, address output) = (path[i], path[i + 1]);
(address token0,) = UniswapV2Library.sortTokens(input, output);
uint amountOut = amounts[i + 1];
(uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0));
address to = i < path.length - 2 ? IUniswapV2Factory(factory).getTokenPair(output, path[i + 2]) : _to;
address pair = IUniswapV2Factory(factory).getTokenPair(input, output);
IUniswapV2Pair(pair).swap(amount0Out, amount1Out, to);
(bool success, bytes memory result) = smartCall(pair, tokens, abi.encodeWithSignature("burn(address)", to));
if (success) {
(amountA, amountB) = abi.decode(result, (uint256, uint256));
} else {
revert("Burn call is not successful");
}
}

// TODO: This method are used for swapping via multiple pairs. Not supported in nil for now
function swapExactTokensForTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
) external override returns (uint[] memory amounts) {
amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
address pair = IUniswapV2Factory(factory).getTokenPair(path[0], path[1]);
function swap(address to, address pair, uint amount0Out, uint amount1Out) public override {
Nil.Token[] memory tokens = Nil.msgTokens();
sendCurrencyInternal(pair, tokens[0].id, amounts[0]);
_swap(amounts, path, to);
if (tokens.length != 1) {
revert("UniswapV2Router: should contains only pair token");
}
smartCall(pair, tokens, abi.encodeWithSignature("swap(uint256,uint256,address)", amount0Out, amount1Out, to));
}

// TODO: This method are used for swapping via multiple pairs. Not supported in nil for now
function swapTokensForExactTokens(
uint amountOut,
uint amountInMax,
address[] calldata path,
address to,
uint deadline
) external override returns (uint[] memory amounts) {
amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
address pair = IUniswapV2Factory(factory).getTokenPair(path[0], path[1]);
function swapExactTokenForTokenSync(
address pair,
uint amountOutMin,
address to
) external override sameShard(pair) returns (uint amount) {
Nil.Token[] memory tokens = Nil.msgTokens();
sendCurrencyInternal(pair, tokens[0].id, amounts[0]);
_swap(amounts, path, to);
if (tokens.length != 1) {
revert("UniswapV2Router: should contains only pair token");
}
uint256 token0Id = IUniswapV2Pair(pair).token0Id();
uint256 token1Id = IUniswapV2Pair(pair).token1Id();
uint256 tokenBId = tokens[0].id != token0Id ? token0Id : token1Id;
(uint reserveA, uint reserveB) = UniswapV2Library.getReserves(pair, tokens[0].id, tokenBId);
amount = UniswapV2Library.getAmountOut(tokens[0].amount, reserveA, reserveB);
require(amount >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
uint amount0Out = tokens[0].id == token0Id ? 0 : amount;
uint amount1Out = tokens[0].id != token0Id ? 0 : amount;
(bool success, bytes memory result) = smartCall(pair, tokens, abi.encodeWithSignature("swap(uint256,uint256,address)", amount0Out, amount1Out, to));
if (!success) {
revert("UniswapV2Router: should get success swap result");
}
}

function quote(uint amountA, uint reserveA, uint reserveB) public pure override returns (uint amountB) {
Expand All @@ -112,13 +163,13 @@ contract UniswapV2Router01 is IUniswapV2Router01, NilCurrencyBase {
receive() external payable {
}

function smartCall(address dst, Nil.Token[] memory tokens, bytes memory callData) private returns (bool) {
function smartCall(address dst, Nil.Token[] memory tokens, bytes memory callData) private returns (bool, bytes memory) {
if (Nil.getShardId(dst) == Nil.getShardId(address(this))) {
(bool success,) = Nil.syncCall(dst, gasleft(), 0, tokens, callData);
return success;
(bool success, bytes memory result) = Nil.syncCall(dst, gasleft(), 0, tokens, callData);
return (success, result);
} else {
Nil.asyncCall(dst, address(0), address(0), 0, Nil.FORWARD_REMAINING, false, 0, tokens, callData);
return true;
return (true, "");
}
}
}
2 changes: 2 additions & 0 deletions contracts/interfaces/IUniswapV2Pair.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ interface IUniswapV2Pair {
function factory() external view returns (address);
function token0() external view returns (address);
function token1() external view returns (address);
function token0Id() external view returns (uint256);
function token1Id() external view returns (uint256);
function getReserves()
external
view
Expand Down
31 changes: 18 additions & 13 deletions contracts/interfaces/IUniswapV2Router01.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,36 @@ interface IUniswapV2Router01 {
address pair,
address to
) external;
function addLiquiditySync(
address pair,
address to,
uint amountADesired,
uint amountBDesired,
uint amountAMin,
uint amountBMin
) external returns (uint amountA, uint amountB);
function removeLiquidity(
address pair,
address to
) external;
function removeLiquiditySync(
address pair,
address to,
uint amountAMin,
uint amountBMin
) external returns (uint amountA, uint amountB);
function swap(
address to,
address pair,
uint amount0Out,
uint amount1Out
) external;

function swapExactTokensForTokens(
uint amountIn,
function swapExactTokenForTokenSync(
address pair,
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
) external returns (uint[] memory amounts);
function swapTokensForExactTokens(
uint amountOut,
uint amountInMax,
address[] calldata path,
address to,
uint deadline
) external returns (uint[] memory amounts);
address to
) external returns (uint amount);

function quote(uint amountA, uint reserveA, uint reserveB) external pure returns (uint amountB);
function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) external pure returns (uint amountOut);
Expand Down
38 changes: 7 additions & 31 deletions contracts/libraries/UniswapV2Library.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,22 @@ pragma solidity ^0.8.0;

import "./SafeMath.sol";
import "../interfaces/IUniswapV2Pair.sol";
import "../interfaces/IUniswapV2Factory.sol";

library UniswapV2Library {
using SafeMath for uint;

// returns sorted token addresses, used to handle return values from pairs sorted in this order
function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) {
require(tokenA != tokenB, 'UniswapV2Library: IDENTICAL_ADDRESSES');
(token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
require(token0 != address(0), 'UniswapV2Library: ZERO_ADDRESS');
function sortTokens(uint256 tokenAId, uint256 tokenBId) internal pure returns (uint256 token0, uint256 token1) {
require(tokenAId != tokenBId, 'UniswapV2Library: IDENTICAL_ADDRESSES');
(token0, token1) = tokenAId < tokenBId ? (tokenAId, tokenBId) : (tokenBId, tokenAId);
require(token0 != uint256(0), 'UniswapV2Library: ZERO_ADDRESS');
}

// fetches and sorts the reserves for a pair
function getReserves(address factory, address tokenA, address tokenB) internal view returns (uint reserveA, uint reserveB) {
(address token0,) = sortTokens(tokenA, tokenB);
address pair = IUniswapV2Factory(factory).getTokenPair(tokenA, tokenB);
function getReserves(address pair, uint256 tokenAId, uint256 tokenBId) internal view returns (uint reserveA, uint reserveB) {
(uint token0,) = sortTokens(tokenAId, tokenBId);
(uint reserve0, uint reserve1) = IUniswapV2Pair(pair).getReserves();
(reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
(reserveA, reserveB) = tokenAId == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
}

// given some amount of an asset and pair reserves, returns an equivalent amount of the other asset
Expand Down Expand Up @@ -48,26 +46,4 @@ library UniswapV2Library {
uint denominator = reserveOut.sub(amountOut).mul(997);
amountIn = (numerator / denominator).add(1);
}

// performs chained getAmountOut calculations on any number of pairs
function getAmountsOut(address factory, uint amountIn, address[] memory path) internal view returns (uint[] memory amounts) {
require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');
amounts = new uint[](path.length);
amounts[0] = amountIn;
for (uint i; i < path.length - 1; i++) {
(uint reserveIn, uint reserveOut) = getReserves(factory, path[i], path[i + 1]);
amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut);
}
}

// performs chained getAmountIn calculations on any number of pairs
function getAmountsIn(address factory, uint amountOut, address[] memory path) internal view returns (uint[] memory amounts) {
require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');
amounts = new uint[](path.length);
amounts[amounts.length - 1] = amountOut;
for (uint i = path.length - 1; i > 0; i--) {
(uint reserveIn, uint reserveOut) = getReserves(factory, path[i - 1], path[i]);
amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut);
}
}
}
Loading
Loading