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