Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions script/Deploy.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,34 @@ import {Drop} from "../src/Drop.sol";
import {ICredentialRegistry} from "bringid/ICredentialRegistry.sol";
import {IERC20} from "openzeppelin/token/ERC20/IERC20.sol";
import {Script, console} from "forge-std/Script.sol";
import "../src/Faucet.sol";
import {EthDrop} from "../src/EthDrop.sol";

contract DeployFaucet is Script {
function run() public {
vm.startBroadcast(vm.envUint("PRIVATE_KEY"));
EthFaucet F = new EthFaucet();
F.transferOwnership(0xBB3D568d557857Ca77772476Ad6edEE88A9BB430);
vm.stopBroadcast();

console.log("EthFaucet:", address(F));
}
}

contract DeployFaucetMainnet is Script {
function run() public {
address CR = vm.envOr("CREDENTIAL_REGISTRY_ADDRESS", address(0));
require(CR != address(0), "CREDENTIAL_REGISTRY_ADDRESS should be provided");

vm.startBroadcast(vm.envUint("PRIVATE_KEY"));
EthDrop ED = new EthDrop(ICredentialRegistry(CR));

ED.transferOwnership(0xBB3D568d557857Ca77772476Ad6edEE88A9BB430);
vm.stopBroadcast();

console.log("EthFaucet:", address(ED));
}
}

contract DeployTopupRun is Script {
function run() public {
Expand Down
84 changes: 84 additions & 0 deletions src/EthDrop.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import {ICredentialRegistry} from "bringid/ICredentialRegistry.sol";
import {Ownable, Ownable2Step} from "openzeppelin/access/Ownable2Step.sol";

error AirdropClaimed(address to);
error AirdropStopped();
error InsufficientScore(uint256 score);
error TransferFailed();
error AmountIsZero();
error ThresholdIsZero();

event Claimed(address to, uint256 amount);
event StatusUpdated(bool isRunning);
event AmountPerClaimUpdated(uint256 amount);
event ScoreThresholdUpdated(uint256 threshold);
event Withdrawn(address to, uint256 amount);

contract EthDrop is Ownable2Step {
ICredentialRegistry public immutable REGISTRY;

mapping(address => bool) public isClaimed;
uint256 public claims;
bool public running = false;
uint256 public amount;
uint256 public scoreThreshold = 10;

constructor(ICredentialRegistry registry) Ownable() {
REGISTRY = registry;
}

function claim(
address to,
ICredentialRegistry.CredentialGroupProof[] calldata proofs
) public {
if(!running) revert AirdropStopped();

if(isClaimed[to]) revert AirdropClaimed(to);
isClaimed[to] = true;

uint256 totalScore = REGISTRY.score(0, proofs);
if(totalScore < scoreThreshold) revert InsufficientScore(totalScore);

uint256 available = address(this).balance;
if (amount >= available) {
amount = available;
running = false;
}

claims++;

(bool success, ) = payable(to).call{value: amount}("");
if(!success) revert TransferFailed();
}

// ONLY OWNER //
function withdraw(
address to,
uint256 amountToWithdraw
) public onlyOwner {
running = false;
(bool success, ) = payable(to).call{value: amountToWithdraw}("");
if(!success) revert TransferFailed();
emit Withdrawn(to, amount);
}

function setAmount(uint256 newAmount) public onlyOwner {
if(newAmount == 0) revert AmountIsZero();
amount = newAmount;
emit AmountPerClaimUpdated(newAmount);
}

function setScoreThreshold(uint256 newThreshold) public onlyOwner {
if(newThreshold == 0) revert ThresholdIsZero();
scoreThreshold = newThreshold;
emit ScoreThresholdUpdated(newThreshold);
}

function setStatus(bool isRunning) public onlyOwner {
running = isRunning;
emit StatusUpdated(isRunning);
}
}
59 changes: 59 additions & 0 deletions src/Faucet.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import {Ownable2Step} from "openzeppelin/access/Ownable2Step.sol";

error TransferFailed();
error RelayerAddressIsZero();
error AmountIsZero();
error SenderIsNotRelayer();

event Claimed(address to, uint256 amount);
event Withdrawn(address to, uint256 amount);
event StatusUpdated(bool status);
event RelayerUpdated(address relayer);
event AmountPerClaimUpdated(uint256 relayer);

contract EthFaucet is Ownable2Step {
bool public running;
address public relayer;
uint256 public amount;

modifier onlyRelayer() {
if (msg.sender != relayer) revert SenderIsNotRelayer();
_;
}

function claim(address to) public onlyRelayer {
(bool success, ) = payable(to).call{value: amount}("");
if(!success) revert TransferFailed();
emit Claimed(to, amount);
}

function withdraw(
address to,
uint256 amountToWithdraw
) public onlyOwner {
running = false;
(bool success, ) = payable(to).call{value: amountToWithdraw}("");
if(!success) revert TransferFailed();
emit Withdrawn(to, amount);
}

function setStatus(bool isRunning) public onlyOwner {
running = isRunning;
emit StatusUpdated(isRunning);
}

function setRelayer(address newRelayer) public onlyOwner {
if(newRelayer == address(0)) revert RelayerAddressIsZero();
relayer = newRelayer;
emit RelayerUpdated(newRelayer);
}

function setAmount(uint256 newAmount) public onlyOwner {
if(newAmount == 0) revert AmountIsZero();
amount = newAmount;
emit AmountPerClaimUpdated(newAmount);
}
}
120 changes: 120 additions & 0 deletions test/Faucet.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import "forge-std/Test.sol";
import {EthFaucet, TransferFailed, RelayerAddressIsZero, AmountIsZero, SenderIsNotRelayer} from "../src/Faucet.sol";

contract FaucetTest is Test {
event Claimed(address to, uint256 amount);
event Withdrawn(address to, uint256 amount);
event StatusUpdated(bool status);
event RelayerUpdated(address relayer);
event AmountPerClaimUpdated(uint256 relayer);

EthFaucet internal faucet;

address internal constant RELAYER = address(0xAAAA);
address internal constant RECIPIENT = address(0xBBBB);
address internal constant WITHDRAW_DEST = address(0xCCCC);
uint256 internal constant DEFAULT_AMOUNT = 1 ether;
uint256 internal constant INITIAL_FAUCET_BALANCE = 10 ether;

function setUp() public {
faucet = new EthFaucet();
faucet.setRelayer(RELAYER);
faucet.setAmount(DEFAULT_AMOUNT);
vm.deal(address(faucet), INITIAL_FAUCET_BALANCE);
}

function testSetStatusUpdatesRunningAndEmitsEvent() public {
vm.expectEmit(false, false, false, true, address(faucet));
emit StatusUpdated(true);
faucet.setStatus(true);
assertTrue(faucet.running());

vm.expectEmit(false, false, false, true, address(faucet));
emit StatusUpdated(false);
faucet.setStatus(false);
assertFalse(faucet.running());
}

function testSetRelayerUpdatesRelayerAndEmitsEvent() public {
address newRelayer = address(0xABCD);
vm.expectEmit(false, false, false, true, address(faucet));
emit RelayerUpdated(newRelayer);
faucet.setRelayer(newRelayer);
assertEq(faucet.relayer(), newRelayer);
}

function testSetRelayerRevertsWhenAddressZero() public {
vm.expectRevert(RelayerAddressIsZero.selector);
faucet.setRelayer(address(0));
}

function testSetAmountUpdatesAmountAndEmitsEvent() public {
uint256 newAmount = 2 ether;
vm.expectEmit(false, false, false, true, address(faucet));
emit AmountPerClaimUpdated(newAmount);
faucet.setAmount(newAmount);
assertEq(faucet.amount(), newAmount);
}

function testSetAmountRevertsWhenZero() public {
vm.expectRevert(AmountIsZero.selector);
faucet.setAmount(0);
}

function testClaimTransfersConfiguredAmountAndEmitsEvent() public {
uint256 recipientBefore = RECIPIENT.balance;

vm.expectEmit(false, false, false, true, address(faucet));
emit Claimed(RECIPIENT, DEFAULT_AMOUNT);

vm.prank(RELAYER);
faucet.claim(RECIPIENT);

assertEq(RECIPIENT.balance - recipientBefore, DEFAULT_AMOUNT);
assertEq(address(faucet).balance, INITIAL_FAUCET_BALANCE - DEFAULT_AMOUNT);
}

function testClaimRevertsWhenCallerNotRelayer() public {
vm.expectRevert(SenderIsNotRelayer.selector);
faucet.claim(RECIPIENT);
}

function testClaimRevertsWhenTransferFails() public {
uint256 failingAmount = INITIAL_FAUCET_BALANCE + 1 ether;
faucet.setAmount(failingAmount);

vm.expectRevert(TransferFailed.selector);
vm.prank(RELAYER);
faucet.claim(RECIPIENT);
}

function testWithdrawSendsFundsAndEmitsEvent() public {
uint256 amountToWithdraw = 3 ether;
faucet.setAmount(amountToWithdraw);
uint256 beforeBalance = WITHDRAW_DEST.balance;

vm.expectEmit(false, false, false, true, address(faucet));
emit Withdrawn(WITHDRAW_DEST, amountToWithdraw);
faucet.withdraw(WITHDRAW_DEST, amountToWithdraw);

assertEq(WITHDRAW_DEST.balance - beforeBalance, amountToWithdraw);
assertEq(address(faucet).balance, INITIAL_FAUCET_BALANCE - amountToWithdraw);
}

function testWithdrawRevertsForNonOwner() public {
vm.expectRevert("Ownable: caller is not the owner");
vm.prank(RELAYER);
faucet.withdraw(WITHDRAW_DEST, 1 ether);
}

function testWithdrawRevertsWhenTransferFails() public {
uint256 amountToWithdraw = INITIAL_FAUCET_BALANCE + 1 ether;
faucet.setAmount(amountToWithdraw);

vm.expectRevert(TransferFailed.selector);
faucet.withdraw(WITHDRAW_DEST, amountToWithdraw);
}
}
Loading