/**
 * SPDX-License-Identifier: MIT
 *
 * Copyright (c) 2018-2020 CENTRE SECZ
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

pragma solidity 0.6.12;

contract FiatTokenUtil {
    // (address,address,uint256,uint256,uint256,bytes32) = 20*2 + 32*4 = 168
    uint256 private constant _TRANSFER_PARAM_SIZE = 168;
    // (uint8,bytes32,bytes32) = 1 + 32*2 = 65
    uint256 private constant _SIGNATURE_SIZE = 65;
    // keccak256("transferWithAuthorization(address,address,uint256,uint256,uint256,bytes32,uint8,bytes32,bytes32)")[0:4]
    bytes4 private constant _TRANSFER_WITH_AUTHORIZATION_SELECTOR = 0xe3ee160e;

    address private _fiatToken;

    event TransferFailed(address indexed authorizer, bytes32 indexed nonce);

    /**
     * @notice Constructor
     * @dev If FiatTokenProxy is used to hold state and delegate calls, the
     * proxy's address should be provided, not the implementation address
     * @param fiatToken Address of the FiatToken contract
     */
    constructor(address fiatToken) public {
        _fiatToken = fiatToken;
    }

    /**
     * @notice Execute multiple authorized ERC20 Transfers
     * @dev The length of params must be multiples of 168, each representing
     * encode-packed data containing from[20] + to[20] + value[32] +
     * validAfter[32] + validBefore[32] + nonce[32], and the length of
     * signatures must be multiples of 65, each representing encode-packed data
     * containing v[1] + r[32] + s[32].
     * @param params      Concatenated, encode-packed parameters
     * @param signatures  Concatenated, encode-packed signatures
     * @param atomic      If true, revert if any of the transfers fail
     * @return            True if every transfer was successful
     */
    function transferWithMultipleAuthorizations(
        bytes calldata params,
        bytes calldata signatures,
        bool atomic
    ) external returns (bool) {
        uint256 num = params.length / _TRANSFER_PARAM_SIZE;
        require(num > 0, "FiatTokenUtil: no transfer provided");
        require(
            num * _TRANSFER_PARAM_SIZE == params.length,
            "FiatTokenUtil: length of params is invalid"
        );
        require(
            signatures.length / _SIGNATURE_SIZE == num &&
                num * _SIGNATURE_SIZE == signatures.length,
            "FiatTokenUtil: length of signatures is invalid"
        );

        uint256 numSuccessful = 0;

        for (uint256 i = 0; i < num; i++) {
            uint256 paramsOffset = i * _TRANSFER_PARAM_SIZE;
            uint256 sigOffset = i * _SIGNATURE_SIZE;

            // extract from and to
            bytes memory fromTo = _unpackAddresses(
                abi.encodePacked(params[paramsOffset:paramsOffset + 40])
            );
            // extract value, validAfter, validBefore, and nonce
            bytes memory other4 = abi.encodePacked(
                params[paramsOffset + 40:paramsOffset + _TRANSFER_PARAM_SIZE]
            );
            // extract v
            uint8 v = uint8(signatures[sigOffset]);
            // extract r and s
            bytes memory rs = abi.encodePacked(
                signatures[sigOffset + 1:sigOffset + _SIGNATURE_SIZE]
            );

            // Call transferWithAuthorization with the extracted parameters
            // solhint-disable-next-line avoid-low-level-calls
            (bool success, bytes memory returnData) = _fiatToken.call(
                abi.encodePacked(
                    _TRANSFER_WITH_AUTHORIZATION_SELECTOR,
                    fromTo,
                    other4,
                    abi.encode(v),
                    rs
                )
            );

            // Revert if atomic is true, and the call was not successful
            if (atomic && !success) {
                _revertWithReasonFromReturnData(returnData);
            }

            // Increment the number of successful transfers
            if (success) {
                numSuccessful++;
            } else {
                // extract from
                (address from, ) = abi.decode(fromTo, (address, address));
                // extract nonce
                (, , , bytes32 nonce) = abi.decode(
                    other4,
                    (uint256, uint256, uint256, bytes32)
                );
                emit TransferFailed(from, nonce);
            }
        }

        // Return true if all transfers were successful
        return numSuccessful == num;
    }

    /**
     * @dev Converts encodePacked pair of addresses (20bytes + 20 bytes) to
     * regular ABI-encoded pair of addresses (32bytes + 32bytes)
     * @param packed Packed data (40 bytes)
     * @return Unpacked data (64 bytes)
     */
    function _unpackAddresses(bytes memory packed)
        private
        pure
        returns (bytes memory)
    {
        address addr1;
        address addr2;
        assembly {
            addr1 := mload(add(packed, 20))
            addr2 := mload(add(packed, 40))
        }
        return abi.encode(addr1, addr2);
    }

    /**
     * @dev Revert with reason string extracted from the return data
     * @param returnData    Return data from a call
     */
    function _revertWithReasonFromReturnData(bytes memory returnData)
        private
        pure
    {
        // Return data will be at least 100 bytes if it contains the reason
        // string: Error(string) selector[4] + string offset[32] + string
        // length[32] + string data[32] = 100
        if (returnData.length < 100) {
            revert("FiatTokenUtil: call failed");
        }

        // If the reason string exists, extract it, and bubble it up
        string memory reason;
        assembly {
            // Skip over the bytes length[32] + Error(string) selector[4] +
            // string offset[32] = 68 (0x44)
            reason := add(returnData, 0x44)
        }

        revert(reason);
    }
}