// File: contracts/Arbrito.sol



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

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

contract Arbrito is IUniswapPairCallee {
  enum Borrow { Token0, Token1 }

  function perform(
    Borrow borrow,
    uint256 amount,
    address uniswapPair,
    address balancerPool,
    address uniswapToken0,
    address uniswapToken1,
    uint256 uniswapReserve0,
    uint256 uniswapReserve1,
    uint256 blockNumber
  ) external {
    require(block.number == blockNumber, "Delayed execution");

    (uint256 reserve0, uint256 reserve1, ) = IUniswapPair(uniswapPair).getReserves();
    require(reserve0 == uniswapReserve0, "Reserve0 mismatch");
    require(reserve1 == uniswapReserve1, "Reserve1 mismatch");

    bytes memory payload =
      abi.encode(
        balancerPool,
        msg.sender,
        uniswapToken0,
        uniswapToken1,
        uniswapReserve0,
        uniswapReserve1
      );

    if (borrow == Borrow.Token0) {
      IUniswapPair(uniswapPair).swap(amount, 0, address(this), payload);
    } else {
      IUniswapPair(uniswapPair).swap(0, amount, address(this), payload);
    }
  }

  function uniswapV2Call(
    address, // sender
    uint256 amount0,
    uint256 amount1,
    bytes calldata data
  ) external override {
    (
      address balancerPoolAddress,
      address ownerAddress,
      address token0,
      address token1,
      uint256 reserve0,
      uint256 reserve1
    ) = abi.decode(data, (address, address, address, address, uint256, uint256));

    uint256 amountTrade;
    uint256 amountPayback;

    address tokenPayback;
    address tokenTrade;

    if (amount0 != 0) {
      amountTrade = amount0;
      (tokenTrade, tokenPayback) = (token0, token1);
      amountPayback = calculateUniswapPayback(amountTrade, reserve1, reserve0);
    } else {
      amountTrade = amount1;
      (tokenPayback, tokenTrade) = (token0, token1);
      amountPayback = calculateUniswapPayback(amountTrade, reserve0, reserve1);
    }

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

    (uint256 balancerAmountOut, ) =
      IBalancerPool(balancerPoolAddress).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.5;

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.5;

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

interface IUniswapPair {
  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.5;

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.5;

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 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.5;

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.5;

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);
  }
}