diff --git a/contracts/Swap.sol b/contracts/Swap.sol new file mode 100644 index 0000000..935965e --- /dev/null +++ b/contracts/Swap.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.6.12; + +import "./libraries/SafeERC20.sol"; +import "./libraries/SafeMath.sol"; + +import "./uniswap/interfaces/IUniswapV2Pair.sol"; +import "./uniswap/interfaces/IUniswapV2Factory.sol"; + +import "./boringcrypto/BoringOwnable.sol"; + +// Swap is a simple token exchange contract built on top on voltage.finance +contract Swap is BoringOwnable { + using SafeMath for uint256; + using SafeERC20 for IERC20; + + IUniswapV2Factory public immutable factory; + + constructor(address _factory) public { + factory = IUniswapV2Factory(_factory); + } + + function swapToken( + address _fromToken, + address _toToken, + uint256 _amountIn, + address _to + ) external onlyOwner { + IERC20(_fromToken).safeTransferFrom(msg.sender, _amountIn); + _swap(_fromToken, _toToken, _amountIn, _to); + } + + function getAmountOut( + address _fromToken, + address _toToken, + uint256 _amountIn + ) public view returns (uint256 amountOut) { + IUniswapV2Pair pair = IUniswapV2Pair(factory.getPair(_fromToken, _toToken)); + if (address(pair) == address(0)) return 0; + + (uint256 reserve0, uint256 reserve1, ) = pair.getReserves(); + + uint256 amountInWithFee = _amountIn.mul(997); + if (_fromToken == pair.token0()) { + amountOut = amountInWithFee.mul(reserve1) / reserve0.mul(1000).add(amountInWithFee); + } else { + amountOut = amountInWithFee.mul(reserve0) / reserve1.mul(1000).add(amountInWithFee); + } + } + + function _swap( + address _fromToken, + address _toToken, + uint256 _amountIn, + address _to + ) internal returns (uint256 realAmountOut) { + IUniswapV2Pair pair = IUniswapV2Pair(factory.getPair(_fromToken, _toToken)); + require(address(pair) != address(0), "VoltMakerV3: Cannot convert"); + + (uint256 reserve0, uint256 reserve1, ) = pair.getReserves(); + + IERC20(_fromToken).safeTransfer(address(pair), _amountIn); + + uint256 balanceBefore = IERC20(_toToken).balanceOf(_to); + + uint256 amountInWithFee = _amountIn.mul(997); + if (_fromToken == pair.token0()) { + uint256 amountOut = amountInWithFee.mul(reserve1) / reserve0.mul(1000).add(amountInWithFee); + pair.swap(0, amountOut, _to, new bytes(0)); + } else { + uint256 amountOut = amountInWithFee.mul(reserve0) / reserve1.mul(1000).add(amountInWithFee); + pair.swap(amountOut, 0, _to, new bytes(0)); + } + + realAmountOut = IERC20(_toToken).balanceOf(_to) - balanceBefore; + } +} diff --git a/test/Swap.test.ts b/test/Swap.test.ts new file mode 100644 index 0000000..b31990c --- /dev/null +++ b/test/Swap.test.ts @@ -0,0 +1,39 @@ +import { network } from "hardhat" +import { expect } from "chai" +import { prepare, deploy, getBigNumber, createSLP } from "./utilities" + +describe("Swap", function () { + before(async function () { + await prepare(this, ["Swap", "ERC20Mock", "UniswapV2Factory", "UniswapV2Pair"]) + }) + + beforeEach(async function () { + await deploy(this, [ + ["volt", this.ERC20Mock, ["VOLT", "VOLT", getBigNumber("100000")]], + ["dai", this.ERC20Mock, ["DAI", "DAI", getBigNumber("10000000")]], + ["usdc", this.ERC20Mock, ["USDC", "USDC", getBigNumber("100000")]], + ["weth", this.ERC20Mock, ["WETH", "ETH", getBigNumber("100000")]], + ["factory", this.UniswapV2Factory, [this.alice.address]], + ]) + + await deploy(this, [["swap", this.Swap, [this.factory.address]]]) + + await createSLP(this, "voltEth", this.volt, this.weth, getBigNumber(100)) + await createSLP(this, "daiUSDC", this.dai, this.usdc, getBigNumber(100)) + }) + + describe("getAmountOut", function () { + it("should return output amount", async function () { + const amountOut = await this.swap.getAmountOut(this.volt.address, this.weth.address, getBigNumber(1)) + expect(amountOut).to.equal('987158034397061298') + }) + }) + + describe("swapToken", function () { + it("should swap from volt to weth", async function () { + await this.volt.approve(this.swap.address, getBigNumber('1')) + await this.swap.swapToken(this.volt.address, this.weth.address, getBigNumber('1'), this.bob.address) + expect(await this.weth.balanceOf(this.bob.address)).to.equal('987158034397061298') + }) + }) +})