// File: contracts/Arbrito.sol



//SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.7.4;

import "./external/IBalancer.sol";
import "./external/IUniswap.sol";
import "./external/IERC20.sol";

contract Arbrito is IUniswapPairCallee {
  function perform(
    bool direction,
    uint256 amount,
    address uniswapPair,
    address balancerPool
  ) external {
    (uint256 amount0, uint256 amount1) = direction
      ? (amount, uint256(0))
      : (uint256(0), amount);

    bytes memory payload = abi.encode(balancerPool, msg.sender);
    IUniswapPair(uniswapPair).swap(amount0, amount1, address(this), payload);
  }

  function uniswapV2Call(
    address, // sender
    uint256 amount0,
    uint256 amount1,
    bytes calldata data
  ) external override {
    (address balancerPoolAddress, address ownerAddress) = abi.decode(
      data,
      (address, address)
    );
    IBalancerPool balancerPool = IBalancerPool(balancerPoolAddress);
    IUniswapPair uniswapPair = IUniswapPair(msg.sender);

    uint256 amountTrade;
    uint256 amountPayback;

    uint256 reservePayback;
    uint256 reserveTrade;

    address tokenPayback;
    address tokenTrade;

    if (amount0 != 0) {
      (reserveTrade, reservePayback, ) = uniswapPair.getReserves();
      tokenPayback = uniswapPair.token1();
      tokenTrade = uniswapPair.token0();
      amountTrade = amount0;
    } else {
      (reservePayback, reserveTrade, ) = uniswapPair.getReserves();
      tokenPayback = uniswapPair.token0();
      tokenTrade = uniswapPair.token1();
      amountTrade = amount1;
    }

    amountPayback = calculateUniswapPayback(
      amountTrade,
      reservePayback,
      reserveTrade
    );

    IERC20(tokenTrade).approve(balancerPoolAddress, amountTrade);

    (uint256 balancerAmountOut, ) = balancerPool.swapExactAmountIn(
      tokenTrade,
      amountTrade,
      tokenPayback,
      amountPayback,
      uint256(-1)
    );

    require(
      IERC20(tokenPayback).transfer(msg.sender, amountPayback),
      "Payback failed"
    );

    require(
      IERC20(tokenPayback).transfer(
        ownerAddress,
        balancerAmountOut - amountPayback
      ),
      "Sender transfer failed"
    );
  }

  function calculateUniswapPayback(
    uint256 amountOut,
    uint256 reserveIn,
    uint256 reserveOut
  ) internal pure returns (uint256) {
    uint256 numerator = reserveIn * amountOut * 1000;
    uint256 denominator = (reserveOut - amountOut) * 997;
    return numerator / denominator + 1;
  }
}

// File: contracts/external/IBalancer.sol



//SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.7.4;

interface IBalancerPool {
  function swapExactAmountIn(
    address tokenIn,
    uint256 tokenAmountIn,
    address tokenOut,
    uint256 minAmountOut,
    uint256 maxPrice
  ) external returns (uint256 tokenAmountOut, uint256 spotPriceAfter);
}

// File: contracts/external/IUniswap.sol



//SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.7.4;

interface IUniswapPairCallee {
  function uniswapV2Call(
    address sender,
    uint256 amount0,
    uint256 amount1,
    bytes calldata data
  ) external;
}

interface IUniswapPair {
  function token0() external view returns (address);

  function token1() external view returns (address);

  function getReserves()
    external
    view
    returns (
      uint112 reserve0,
      uint112 reserve1,
      uint32 blockTimestampLast
    );

  function swap(
    uint256 amount0Out,
    uint256 amount1Out,
    address to,
    bytes calldata data
  ) external;
}

// File: contracts/external/IERC20.sol



//SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.7.4;

interface IERC20 {
  function approve(address spender, uint256 amount) external returns (bool);
  function transfer(address recipient, uint256 amount) external returns (bool);
}

// File: test/external/Uniswap.sol



//SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.7.4;

import "./IERC20.sol";
import "../../contracts/external/IUniswap.sol";

contract Uniswap is IUniswapPair {
  address token0address;
  address token1address;
  uint112 reserve0;
  uint112 reserve1;

  constructor(address _token0, address _token1) {
    token0address = _token0;
    token1address = _token1;
  }

  function token0() external view override returns (address) {
    return token0address;
  }

  function token1() external view override returns (address) {
    return token1address;
  }

  function getReserves()
    external
    view
    override
    returns (
      uint112,
      uint112,
      uint32
    )
  {
    return (reserve0, reserve1, 0);
  }

  function refreshReserves() external {
    address me = address(this);
    reserve0 = uint112(IERC20(token0address).balanceOf(me));
    reserve1 = uint112(IERC20(token1address).balanceOf(me));
  }

  function swap(
    uint256 amount0,
    uint256 amount1,
    address receiver,
    bytes calldata payload
  ) external override {
    require(payload.length != 0, "Unsupported payload");
    require(
      (amount0 == 0 && amount1 != 0) || (amount0 != 0 && amount1 == 0),
      "Unsupported amounts"
    );

    IERC20 tokenLent;
    IERC20 tokenPayback;
    uint256 amountLent;

    if (amount0 != 0) {
      tokenLent = IERC20(token0address);
      tokenPayback = IERC20(token1address);
      amountLent = amount0;
      require(IERC20(token0address).transfer(receiver, amount0), "loan failed");
    } else {
      tokenLent = IERC20(token1address);
      tokenPayback = IERC20(token0address);
      amountLent = amount1;
      require(IERC20(token1address).transfer(receiver, amount1), "loan failed");
    }

    address me = address(this);
    uint256 tokenLentBalance = tokenLent.balanceOf(me);
    uint256 tokenPaybackBalance = tokenPayback.balanceOf(me);

    IUniswapPairCallee(msg.sender).uniswapV2Call(
      msg.sender,
      amount0,
      amount1,
      payload
    );

    reserve0 -= uint112(amount0);
    reserve1 -= uint112(amount1);

    require(tokenLent.balanceOf(me) == tokenLentBalance, "unsupported payback");

    uint256 tokenPaybackBalanceAfter = tokenPayback.balanceOf(me);
    require(tokenPaybackBalanceAfter > tokenPaybackBalance, "missing payback");

    uint256 amountPaidBack = tokenPaybackBalanceAfter - tokenPaybackBalance;
    uint256 balance0Adjusted = tokenPaybackBalanceAfter *
      1000 -
      amountPaidBack *
      3;
    uint256 balance1Adjusted = tokenLentBalance;
    require(
      balance0Adjusted * balance1Adjusted >=
        (tokenLentBalance + amountLent) * tokenPaybackBalance * 1000,
      "payback mismatch"
    );
  }
}

// File: test/external/IERC20.sol



//SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.7.4;

interface IERC20 {
  function transferFrom(
    address sender,
    address recipient,
    uint256 amount
  ) external returns (bool);

  function transfer(address recipient, uint256 amount) external returns (bool);

  function balanceOf(address account) external view returns (uint256);

  function allowance(address owner, address spender)
    external
    view
    returns (uint256);

  function approve(address spender, uint256 amount) external returns (bool);
}

contract ERC20 is IERC20 {
  mapping(address => uint256) balances;
  mapping(address => mapping(address => uint256)) allowances;

  function balanceOf(address account) external view override returns (uint256) {
    return balances[account];
  }

  function transfer(address recipient, uint256 amount)
    external
    override
    returns (bool)
  {
    if (balances[msg.sender] < amount) {
      return false;
    }

    balances[msg.sender] -= amount;
    balances[recipient] += amount;
    return true;
  }

  function transferFrom(
    address sender,
    address recipient,
    uint256 amount
  ) external override returns (bool) {
    require(allowances[sender][msg.sender] >= amount, "No allowance");
    allowances[sender][msg.sender] -= amount;

    if (balances[sender] < amount) {
      return false;
    }

    balances[sender] -= amount;
    balances[recipient] += amount;
    return true;
  }

  function allowance(address owner, address spender)
    external
    view
    override
    returns (uint256)
  {
    return allowances[owner][spender];
  }

  function approve(address spender, uint256 amount)
    external
    override
    returns (bool)
  {
    allowances[msg.sender][spender] = amount;
    return true;
  }
}

contract ERC20Mintable is ERC20 {
  function mint(address account, uint256 amount) external {
    balances[account] += amount;
  }
}

// File: test/external/Balancer.sol



//SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.7.4;

import "./IERC20.sol";
import "../../contracts/external/IBalancer.sol";

contract Balancer is IBalancerPool {
  function swapExactAmountIn(
    address _tokenIn,
    uint256 _tokenAmountIn,
    address _tokenOut,
    uint256 _minAmountOut,
    uint256 _maxPrice
  ) external override returns (uint256, uint256) {
    require(_maxPrice == uint256(-1), "Unsupported maxPrice");

    address me = address(this);
    IERC20 tokenIn = IERC20(_tokenIn);
    IERC20 tokenOut = IERC20(_tokenOut);

    uint256 tokenAmountOut = (10**18 * tokenOut.balanceOf(me) * _tokenAmountIn);
    tokenAmountOut /= (10**18 * tokenIn.balanceOf(me));

    require(tokenAmountOut > _minAmountOut, "Insufficient amount out");

    require(
      tokenIn.transferFrom(msg.sender, me, _tokenAmountIn),
      "Transfer in failed"
    );

    require(
      tokenOut.transfer(msg.sender, tokenAmountOut),
      "Transfer out failed"
    );

    return (tokenAmountOut, 0);
  }
}