Commit 3d4019d6 authored by tre's avatar tre

Address comments and split AuthModule into separate file

parent 5943c418
//SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import { Test } from "forge-std/Test.sol";
import { AdminFaucetAuthModule } from "../universal/faucet/authmodules/AdminFaucetAuthModule.sol";
import { Faucet } from "../universal/faucet/Faucet.sol";
import { FaucetHelper } from "../testing/helpers/FaucetHelper.sol";
/**
* @title AdminFaucetAuthModuleTest
* @notice Tests the AdminFaucetAuthModule contract.
*/
contract AdminFaucetAuthModuleTest is Test {
/**
* @notice The admin of the `AdminFaucetAuthModule` contract.
*/
address internal admin;
/**
* @notice Private key of the `admin`.
*/
uint256 internal adminKey;
/**
* @notice Not an admin of the `AdminFaucetAuthModule` contract.
*/
address internal nonAdmin;
/**
* @notice Private key of the `nonAdmin`.
*/
uint256 internal nonAdminKey;
/**
* @notice An instance of the `AdminFaucetAuthModule` contract.
*/
AdminFaucetAuthModule adminFam;
/**
* @notice An instance of the `FaucetHelper` contract.
*/
FaucetHelper faucetHelper;
/**
* @notice Deploy the `AdminFaucetAuthModule` contract.
*/
function setUp() external {
adminKey = 0xB0B0B0B0;
admin = vm.addr(adminKey);
nonAdminKey = 0xC0C0C0C0;
nonAdmin = vm.addr(nonAdminKey);
adminFam = new AdminFaucetAuthModule(admin);
adminFam.initialize("AdminFAM");
faucetHelper = new FaucetHelper();
}
/**
* @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,
bytes32 nonce
) internal view returns (bytes memory) {
AdminFaucetAuthModule.Proof memory proof = AdminFaucetAuthModule.Proof(
recipient,
nonce,
id
);
return
_getSignature(
_issuerPrivateKey,
faucetHelper.getDigestWithEIP712Domain(
proof,
_eip712Name,
_contractVersion,
_eip712Chainid,
_eip712VerifyingContract
)
);
}
/**
* @notice assert that verify returns true for valid proofs signed by admins.
*/
function test_adminProof_verify_returnsTrue() external {
bytes32 nonce = faucetHelper.consumeNonce();
address fundsReceiver = makeAddr("fundsReceiver");
bytes memory proof = issueProofWithEIP712Domain(
adminKey,
bytes("AdminFAM"),
bytes(adminFam.version()),
block.chainid,
address(adminFam),
fundsReceiver,
abi.encodePacked(fundsReceiver),
nonce
);
vm.prank(nonAdmin);
adminFam.verify(
Faucet.DripParameters(payable(fundsReceiver), nonce),
abi.encodePacked(fundsReceiver),
proof
);
}
/**
* @notice assert that verify returns false for proofs signed by nonadmins.
*/
function test_nonAdminProof_verify_returnsFalse() external {
bytes32 nonce = faucetHelper.consumeNonce();
address fundsReceiver = makeAddr("fundsReceiver");
bytes memory proof = issueProofWithEIP712Domain(
nonAdminKey,
bytes("AdminFAM"),
bytes(adminFam.version()),
block.chainid,
address(adminFam),
fundsReceiver,
abi.encodePacked(fundsReceiver),
nonce
);
vm.prank(admin);
assertEq(
adminFam.verify(
Faucet.DripParameters(payable(fundsReceiver), nonce),
abi.encodePacked(fundsReceiver),
proof
),
false
);
}
/**
* @notice assert that verify returns false for proofs where the id in the proof is different
* than the id in the call to verify.
*/
function test_proofWithWrongId_verify_returnsFalse() external {
bytes32 nonce = faucetHelper.consumeNonce();
address fundsReceiver = makeAddr("fundsReceiver");
address randomAddress = makeAddr("randomAddress");
bytes memory proof = issueProofWithEIP712Domain(
adminKey,
bytes("AdminFAM"),
bytes(adminFam.version()),
block.chainid,
address(adminFam),
fundsReceiver,
abi.encodePacked(fundsReceiver),
nonce
);
vm.prank(admin);
assertEq(
adminFam.verify(
Faucet.DripParameters(payable(fundsReceiver), nonce),
abi.encodePacked(randomAddress),
proof
),
false
);
}
}
......@@ -2,7 +2,8 @@
pragma solidity 0.8.15;
import { Test } from "forge-std/Test.sol";
import { AdminFAM, Faucet } from "../universal/faucet/Faucet.sol";
import { Faucet } from "../universal/faucet/Faucet.sol";
import { AdminFaucetAuthModule } from "../universal/faucet/authmodules/AdminFaucetAuthModule.sol";
import { FaucetHelper } from "../testing/helpers/FaucetHelper.sol";
contract Faucet_Initializer is Test {
......@@ -14,7 +15,7 @@ contract Faucet_Initializer is Test {
uint256 internal nonAdminKey;
Faucet faucet;
AdminFAM adminFam;
AdminFaucetAuthModule adminFam;
FaucetHelper faucetHelper;
......@@ -40,16 +41,15 @@ contract Faucet_Initializer is Test {
// Fill faucet with ether.
vm.deal(address(faucet), 10 ether);
adminFam = new AdminFAM(faucetAuthAdmin);
adminFam = new AdminFaucetAuthModule(faucetAuthAdmin);
adminFam.initialize("AdminFAM");
faucetHelper = new FaucetHelper();
}
function _enableFaucetAuthModule() internal {
vm.prank(faucetContractAdmin);
faucet.configure(adminFam, Faucet.ModuleConfig(true, 1 days, 1 ether));
faucet.configure(adminFam, Faucet.ModuleConfig("OptimistModule", true, 1 days, 1 ether));
}
/**
......@@ -82,7 +82,11 @@ contract Faucet_Initializer is Test {
bytes memory id,
bytes32 nonce
) internal view returns (bytes memory) {
AdminFAM.Proof memory proof = AdminFAM.Proof(recipient, nonce, id);
AdminFaucetAuthModule.Proof memory proof = AdminFaucetAuthModule.Proof(
recipient,
nonce,
id
);
return
_getSignature(
_issuerPrivateKey,
......@@ -105,8 +109,7 @@ contract FaucetTest is Faucet_Initializer {
function test_AuthAdmin_drip_succeeds() external {
_enableFaucetAuthModule();
bytes32 nonce = faucetHelper.consumeNonce();
bytes memory signature
= issueProofWithEIP712Domain(
bytes memory signature = issueProofWithEIP712Domain(
faucetAuthAdminKey,
bytes("AdminFAM"),
bytes(adminFam.version()),
......@@ -120,14 +123,14 @@ contract FaucetTest is Faucet_Initializer {
vm.prank(nonAdmin);
faucet.drip(
Faucet.DripParameters(payable(fundsReceiver), nonce),
Faucet.AuthParameters(adminFam, abi.encodePacked(fundsReceiver), signature));
Faucet.AuthParameters(adminFam, abi.encodePacked(fundsReceiver), signature)
);
}
function test_nonAdmin_drip_fails() external {
_enableFaucetAuthModule();
bytes32 nonce = faucetHelper.consumeNonce();
bytes memory signature
= issueProofWithEIP712Domain(
bytes memory signature = issueProofWithEIP712Domain(
nonAdminKey,
bytes("AdminFAM"),
bytes(adminFam.version()),
......@@ -142,6 +145,7 @@ contract FaucetTest is Faucet_Initializer {
vm.expectRevert("Faucet: drip parameters could not be verified by security module");
faucet.drip(
Faucet.DripParameters(payable(fundsReceiver), nonce),
Faucet.AuthParameters(adminFam, abi.encodePacked(fundsReceiver), signature));
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";
import {
AdminFaucetAuthModule
} from "../../universal/faucet/authmodules/AdminFaucetAuthModule.sol";
import {
ECDSAUpgradeable
} from "@openzeppelin/contracts-upgradeable/utils/cryptography/ECDSAUpgradeable.sol";
/**
* Simple helper contract that helps with testing the Faucet contract.
......@@ -44,20 +48,12 @@ contract FaucetHelper {
*
* @return EIP-712 typed struct hash.
*/
function getProofStructHash(AdminFAM.Proof memory _proof)
function getProofStructHash(AdminFaucetAuthModule.Proof memory _proof)
public
pure
returns (bytes32)
{
return
keccak256(
abi.encode(
PROOF_TYPEHASH,
_proof.recipient,
_proof.nonce,
_proof.id
)
);
return keccak256(abi.encode(PROOF_TYPEHASH, _proof.recipient, _proof.nonce, _proof.id));
}
/**
......@@ -75,7 +71,7 @@ contract FaucetHelper {
* @return EIP-712 compatible digest.
*/
function getDigestWithEIP712Domain(
AdminFAM.Proof memory _proof,
AdminFaucetAuthModule.Proof memory _proof,
bytes memory _name,
bytes memory _version,
uint256 _chainid,
......@@ -90,8 +86,6 @@ contract FaucetHelper {
_verifyingContract
)
);
return
ECDSA.toTypedDataHash(domainSeparator, getProofStructHash(_proof));
return ECDSAUpgradeable.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 { SignatureChecker } from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";
import {
EIP712Upgradeable
} from "@openzeppelin/contracts-upgradeable/utils/cryptography/draft-EIP712Upgradeable.sol";
import { IFaucetAuthModule } from "./authmodules/IFaucetAuthModule.sol";
/**
* @title SafeSend
......@@ -13,110 +9,10 @@ import {
*/
contract SafeSend {
/**
* @param recipient Account to send ETH to.
* @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 Proof type.
*/
bytes32 public constant PROOF_TYPEHASH =
keccak256("Proof(address recipient,bytes32 nonce,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);
constructor(address payable _recipient) payable {
selfdestruct(_recipient);
}
}
......@@ -137,7 +33,7 @@ contract Faucet {
* @notice Parameters for authentication.
*/
struct AuthParameters {
FaucetAuthModule module;
IFaucetAuthModule module;
bytes id;
bytes proof;
}
......@@ -146,6 +42,7 @@ contract Faucet {
* @notice Configuration for an authentication module.
*/
struct ModuleConfig {
string name;
bool enabled;
uint256 ttl;
uint256 amount;
......@@ -159,12 +56,12 @@ contract Faucet {
/**
* @notice Mapping of authentication modules to their configurations.
*/
mapping (FaucetAuthModule => ModuleConfig) public modules;
mapping(IFaucetAuthModule => 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;
mapping(IFaucetAuthModule => mapping(bytes => uint256)) public timeouts;
/**
* @notice Maps from id to nonces to whether or not they have been used.
......@@ -175,20 +72,15 @@ contract Faucet {
* @notice Modifier that makes a function admin priviledged.
*/
modifier priviledged() {
require(
msg.sender == ADMIN,
"Faucet: function can only be called by admin"
);
require(msg.sender == ADMIN, "Faucet: function can only be called by admin");
_;
}
/**
* @param admin Admin address that can configure the faucet.
* @param _admin Admin address that can configure the faucet.
*/
constructor(
address admin
) {
ADMIN = admin;
constructor(address _admin) {
ADMIN = _admin;
}
/**
......@@ -201,73 +93,62 @@ contract Faucet {
/**
* @notice Allows the admin to withdraw funds.
*
* @param recipient Address to receive the funds.
* @param amount Amount of ETH in wei to withdraw.
* @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);
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.
* @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;
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.
* @param _params Drip parameters.
* @param _auth Authentication parameters.
*/
function drip(
DripParameters memory params,
AuthParameters memory auth
) public {
function drip(DripParameters memory _params, AuthParameters memory _auth) public {
// Grab the module config once.
ModuleConfig memory config = modules[auth.module];
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"
);
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,
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,
timeouts[_auth.module][_auth.id] == 0 ||
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),
_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;
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);
new SafeSend{ value: config.amount }(_params.recipient);
}
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import { Semver } from "@eth-optimism/contracts-bedrock/contracts/universal/Semver.sol";
import {
EIP712Upgradeable
} from "@openzeppelin/contracts-upgradeable/utils/cryptography/draft-EIP712Upgradeable.sol";
import { SignatureChecker } from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";
import { IFaucetAuthModule } from "./IFaucetAuthModule.sol";
import { Faucet } from "../Faucet.sol";
/**
* @title AdminFaucetAuthModule
* @notice FaucetAuthModule that allows an admin to sign off on a given faucet drip. Takes an admin
* as the constructor argument.
*/
contract AdminFaucetAuthModule is IFaucetAuthModule, Semver, EIP712Upgradeable {
/**
* @notice Admin address that can sign off on drips.
*/
address public immutable ADMIN;
/**
* @notice EIP712 typehash for the Proof type.
*/
bytes32 public constant PROOF_TYPEHASH =
keccak256("Proof(address recipient,bytes32 nonce,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 IFaucetAuthModule
*/
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);
}
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import { Faucet } from "../Faucet.sol";
/**
* @title IFaucetAuthModule
* @notice Interface for faucet authentication modules.
*/
interface IFaucetAuthModule {
/**
* @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);
}
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