Commit 49806f74 authored by tre's avatar tre

Create initial contract

parent c3df5d35
//SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
/* Testing utilities */
import { Test } from "forge-std/Test.sol";
import { AdminFAM, Faucet } from "../universal/faucet/Faucet.sol";
import { FaucetHelper } from "../testing/helpers/FaucetHelper.sol";
contract Faucet_Initializer is Test {
address internal faucetContractAdmin;
address internal faucetAuthAdmin;
address internal nonAdmin;
address internal fundsReceiver;
uint256 internal faucetAuthAdminKey;
uint256 internal nonAdminKey;
Faucet faucet;
AdminFAM adminFam;
FaucetHelper faucetHelper;
function setUp() public {
faucetContractAdmin = makeAddr("faucetContractAdmin");
fundsReceiver = makeAddr("fundsReceiver");
faucetAuthAdminKey = 0xB0B0B0B0;
faucetAuthAdmin = vm.addr(faucetAuthAdminKey);
nonAdminKey = 0xC0C0C0C0;
nonAdmin = vm.addr(nonAdminKey);
_initializeContracts();
}
/**
* @notice Instantiates a Faucet.
*/
function _initializeContracts() internal {
faucet = new Faucet(faucetContractAdmin);
// Fill faucet with ether.
vm.deal(address(faucet), 10 ether);
adminFam = new AdminFAM(faucetAuthAdmin);
adminFam.initialize("AdminFAM");
faucetHelper = new FaucetHelper();
}
function _enableFaucetAuthModule() internal {
vm.prank(faucetContractAdmin);
faucet.configure(adminFam, Faucet.ModuleConfig(true, 1 days, 1 ether));
}
/**
* @notice Get signature as a bytes blob.
*
*/
function _getSignature(uint256 _signingPrivateKey, bytes32 _digest)
internal
pure
returns (bytes memory)
{
(uint8 v, bytes32 r, bytes32 s) = vm.sign(_signingPrivateKey, _digest);
bytes memory signature = abi.encodePacked(r, s, v);
return signature;
}
/**
* @notice Signs a proof with the given private key and returns the signature using
* the given EIP712 domain separator. This assumes that the issuer's address is the
* corresponding public key to _issuerPrivateKey.
*/
function issueProofWithEIP712Domain(
uint256 _issuerPrivateKey,
bytes memory _eip712Name,
bytes memory _contractVersion,
uint256 _eip712Chainid,
address _eip712VerifyingContract,
address recipient,
bytes memory id,
uint256 nonce
) internal view returns (bytes memory) {
AdminFAM.Proof memory proof = AdminFAM.Proof(recipient, bytes32(keccak256(abi.encode(nonce))), id);
return
_getSignature(
_issuerPrivateKey,
faucetHelper.getDigestWithEIP712Domain(
proof,
_eip712Name,
_contractVersion,
_eip712Chainid,
_eip712VerifyingContract
)
);
}
}
contract FaucetTest is Faucet_Initializer {
function test_initialize() external {
assertEq(faucet.ADMIN(), faucetContractAdmin);
}
function test_AuthAdmin_drip_succeeds() external {
_enableFaucetAuthModule();
bytes memory signature
= issueProofWithEIP712Domain(
faucetAuthAdminKey,
bytes("AdminFAM"),
bytes(adminFam.version()),
block.chainid,
address(adminFam),
fundsReceiver,
abi.encodePacked(fundsReceiver),
faucetHelper.currentNonce()
);
vm.prank(nonAdmin);
faucet.drip(
Faucet.DripParameters(payable(fundsReceiver), faucetHelper.consumeNonce()),
Faucet.AuthParameters(adminFam, abi.encodePacked(fundsReceiver), signature));
}
function test_nonAdmin_drip_fails() external {
_enableFaucetAuthModule();
bytes memory signature
= issueProofWithEIP712Domain(
nonAdminKey,
bytes("AdminFAM"),
bytes(adminFam.version()),
block.chainid,
address(adminFam),
fundsReceiver,
abi.encodePacked(fundsReceiver),
faucetHelper.currentNonce()
);
vm.prank(nonAdmin);
vm.expectRevert("Faucet: drip parameters could not be verified by security module");
faucet.drip(
Faucet.DripParameters(payable(fundsReceiver), faucetHelper.consumeNonce()),
Faucet.AuthParameters(adminFam, abi.encodePacked(fundsReceiver), signature));
}
}
//SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import { AdminFAM } from "../../universal/faucet/Faucet.sol";
import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
/**
* Simple helper contract that helps with testing the Faucet contract.
*/
contract FaucetHelper {
/**
* @notice EIP712 typehash for the ClaimableInvite type.
*/
bytes32 public constant PROOF_TYPEHASH =
keccak256("Proof(address recipient,bytes32 nonce,bytes id)");
/**
* @notice EIP712 typehash for the EIP712Domain type that is included as part of the signature.
*/
bytes32 public constant EIP712_DOMAIN_TYPEHASH =
keccak256(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
);
/**
* @notice Keeps track of current nonce to generate new nonces for each invite.
*/
uint256 public currentNonce;
/**
* @notice Returns a bytes32 nonce that should change everytime. In practice, people should use
* pseudorandom nonces.
*
* @return Nonce that should be used as part of drip parameters.
*/
function consumeNonce() public returns (bytes32) {
return bytes32(keccak256(abi.encode(currentNonce++)));
}
/**
* @notice Returns the hash of the struct ClaimableInvite.
*
* @param _proof ClaimableInvite struct to hash.
*
* @return EIP-712 typed struct hash.
*/
function getProofStructHash(AdminFAM.Proof memory _proof)
public
pure
returns (bytes32)
{
return
keccak256(
abi.encode(
PROOF_TYPEHASH,
_proof.recipient,
_proof.nonce,
_proof.id
)
);
}
/**
* @notice Computes the EIP712 digest with the given domain parameters.
* Used for testing that different domain parameters fail.
*
* @param _proof ClaimableInvite struct to hash.
* @param _name Contract name to use in the EIP712 domain.
* @param _version Contract version to use in the EIP712 domain.
* @param _chainid Chain ID to use in the EIP712 domain.
* @param _verifyingContract Address to use in the EIP712 domain.
* @param _verifyingContract Address to use in the EIP712 domain.
* @param _verifyingContract Address to use in the EIP712 domain.
*
* @return EIP-712 compatible digest.
*/
function getDigestWithEIP712Domain(
AdminFAM.Proof memory _proof,
bytes memory _name,
bytes memory _version,
uint256 _chainid,
address _verifyingContract
) public pure returns (bytes32) {
bytes32 domainSeparator = keccak256(
abi.encode(
EIP712_DOMAIN_TYPEHASH,
keccak256(_name),
keccak256(_version),
_chainid,
_verifyingContract
)
);
return
ECDSA.toTypedDataHash(domainSeparator, getProofStructHash(_proof));
}
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import { Semver } from "@eth-optimism/contracts-bedrock/contracts/universal/Semver.sol";
import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import { SignatureChecker } from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";
import {
EIP712Upgradeable
} from "@openzeppelin/contracts-upgradeable/utils/cryptography/draft-EIP712Upgradeable.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 FaucetAuthModule
* @notice Interface for faucet authentication modules.
*/
interface FaucetAuthModule {
/**
* @notice Verifies that the given drip parameters are valid.
*
* @param params Drip parameters to verify.
* @param id Authentication ID to verify.
* @param proof Authentication proof to verify.
*/
function verify(
Faucet.DripParameters memory params,
bytes memory id,
bytes memory proof
)
external
view
returns (
bool
);
}
/**
* @title AdminFAM
* @notice FaucetAuthModule that allows an admin to sign off on a given faucet drip. Takes an admin
* as the constructor argument.
*/
contract AdminFAM is FaucetAuthModule, Semver, EIP712Upgradeable {
/**
* @notice Admin address that can sign off on drips.
*/
address public immutable ADMIN;
/**
* @notice EIP712 typehash for the ClaimableInvite type.
*/
bytes32 public constant PROOF_TYPEHASH =
keccak256("Proof(address recipient,bytes32 nonce,bytes id)");
// bytes32 public constant PROOF_TYPEHASH =
// keccak256("Proof(address recipient,bytes id)");
/**
* @notice Struct that represents a proof that verifies the admin.
*
* @custom:field recipient Address that will be receiving the faucet funds.
* @custom:field nonce Pseudorandom nonce to prevent replay attacks.
* @custom:field id id for the user requesting the faucet funds.
*/
struct Proof {
address recipient;
bytes32 nonce;
bytes id;
}
/**
* @param admin Admin address that can sign off on drips.
*/
constructor(
address admin
) Semver(1, 0, 0) {
ADMIN = admin;
}
/**
* @notice Initializes this contract, setting the EIP712 context.
*
* @param _name Contract name.
*/
function initialize(string memory _name) public initializer {
__EIP712_init(_name, version());
}
/**
* @inheritdoc FaucetAuthModule
*/
function verify(
Faucet.DripParameters memory params,
bytes memory id,
bytes memory proof
)
external
view
returns (
bool
)
{
// Generate a EIP712 typed data hash to compare against the proof.
bytes32 digest = _hashTypedDataV4(
keccak256(
abi.encode(
PROOF_TYPEHASH,
params.recipient,
params.nonce,
id
)
)
);
return SignatureChecker.isValidSignatureNow(ADMIN, digest, proof);
}
}
/**
* @title Faucet
* @notice Faucet contract that drips ETH to users.
*/
contract Faucet {
/**
* @notice Parameters for a drip.
*/
struct DripParameters {
address payable recipient;
bytes32 nonce;
}
/**
* @notice Parameters for authentication.
*/
struct AuthParameters {
FaucetAuthModule module;
bytes id;
bytes proof;
}
/**
* @notice Configuration for an authentication module.
*/
struct ModuleConfig {
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 (FaucetAuthModule => ModuleConfig) public modules;
/**
* @notice Mapping of authentication IDs to the next timestamp at which they can be used.
*/
mapping (FaucetAuthModule => mapping (bytes => uint256)) public timeouts;
/**
* @notice Maps from id to nonces to whether or not they have been used.
*/
mapping(bytes => mapping(bytes32 => bool)) public usedNonces;
/**
* @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(
FaucetAuthModule 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(
usedNonces[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;
// Execute a safe transfer of ETH to the recipient account.
new SafeSend{value: config.amount}(params.recipient);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment