// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;

import { IFaucetAuthModule } from "./authmodules/IFaucetAuthModule.sol";

/**
 * @title  SafeSend
 * @notice Sends ETH to a recipient account without triggering any code.
 */
contract SafeSend {
    /**
     * @param _recipient Account to send ETH to.
     */
    constructor(address payable _recipient) payable {
        selfdestruct(_recipient);
    }
}

/**
 * @title  Faucet
 * @notice Faucet contract that drips ETH to users.
 */
contract Faucet {
    /**
     * @notice Emitted on each drip.
     *
     * @param authModule The type of authentication that was used for verifying the drip.
     * @param userId     The id of the user that requested the drip.
     * @param amount     The amount of funds sent.
     * @param recipient  The recipient of the drip.
     */
    event Drip(
        string indexed authModule,
        bytes32 indexed userId,
        uint256 amount,
        address indexed recipient
    );

    /**
     * @notice Parameters for a drip.
     */
    struct DripParameters {
        address payable recipient;
        bytes32 nonce;
    }

    /**
     * @notice Parameters for authentication.
     */
    struct AuthParameters {
        IFaucetAuthModule module;
        bytes32 id;
        bytes proof;
    }

    /**
     * @notice Configuration for an authentication module.
     */
    struct ModuleConfig {
        string name;
        bool enabled;
        uint256 ttl;
        uint256 amount;
    }

    /**
     * @notice Admin address that can configure the faucet.
     */
    address public immutable ADMIN;

    /**
     * @notice Mapping of authentication modules to their configurations.
     */
    mapping(IFaucetAuthModule => ModuleConfig) public modules;

    /**
     * @notice Mapping of authentication IDs to the next timestamp at which they can be used.
     */
    mapping(IFaucetAuthModule => mapping(bytes32 => uint256)) public timeouts;

    /**
     * @notice Maps from id to nonces to whether or not they have been used.
     */
    mapping(bytes32 => mapping(bytes32 => bool)) public nonces;

    /**
     * @notice Modifier that makes a function admin priviledged.
     */
    modifier priviledged() {
        require(msg.sender == ADMIN, "Faucet: function can only be called by admin");
        _;
    }

    /**
     * @param _admin Admin address that can configure the faucet.
     */
    constructor(address _admin) {
        ADMIN = _admin;
    }

    /**
     * @notice Allows users to donate ETH to this contract.
     */
    receive() external payable {
        // Thank you!
    }

    /**
     * @notice Allows the admin to withdraw funds.
     *
     * @param _recipient Address to receive the funds.
     * @param _amount    Amount of ETH in wei to withdraw.
     */
    function withdraw(address payable _recipient, uint256 _amount) public priviledged {
        new SafeSend{ value: _amount }(_recipient);
    }

    /**
     * @notice Allows the admin to configure an authentication module.
     *
     * @param _module Authentication module to configure.
     * @param _config Configuration to set for the module.
     */
    function configure(IFaucetAuthModule _module, ModuleConfig memory _config) public priviledged {
        modules[_module] = _config;
    }

    /**
     * @notice Drips ETH to a recipient account.
     *
     * @param _params Drip parameters.
     * @param _auth   Authentication parameters.
     */
    function drip(DripParameters memory _params, AuthParameters memory _auth) public {
        // Grab the module config once.
        ModuleConfig memory config = modules[_auth.module];

        // Make sure we're using a supported security module.
        require(config.enabled, "Faucet: provided auth module is not supported by this faucet");

        // The issuer's signature commits to a nonce to prevent replay attacks.
        // This checks that the nonce has not been used for this issuer before. The nonces are
        // scoped to the issuer address, so the same nonce can be used by different issuers without
        // clashing.
        require(nonces[_auth.id][_params.nonce] == false, "Faucet: nonce has already been used");

        // Make sure the timeout has elapsed.
        require(
            timeouts[_auth.module][_auth.id] < block.timestamp,
            "Faucet: auth cannot be used yet because timeout has not elapsed"
        );

        // Verify the proof.
        require(
            _auth.module.verify(_params, _auth.id, _auth.proof),
            "Faucet: drip parameters could not be verified by security module"
        );

        // Set the next timestamp at which this auth id can be used.
        timeouts[_auth.module][_auth.id] = block.timestamp + config.ttl;

        // Mark the nonce as used.
        nonces[_auth.id][_params.nonce] = true;

        // Execute a safe transfer of ETH to the recipient account.
        new SafeSend{ value: config.amount }(_params.recipient);

        emit Drip(config.name, _auth.id, config.amount, _params.recipient);
    }
}
