Raisebox Faucet

First Flight #50
Beginner FriendlySolidity
100 EXP
View results
Submission Details
Impact: medium
Likelihood: medium
Invalid

Failure of ETH issuance will interrupt ERC20 issuance

Root + Impact

Description

  • If the faucetClaimer is a non-payable contract (i.e., it has no receive() or fallback() functions), the entire claimFaucetTokens() function is rolled back.

    All state changes (including ERC20 token issuance) are undone.

(bool success,) = faucetClaimer.call{value: sepEthAmountToDrip}("");
if (success) {
emit SepEthDripped(faucetClaimer, sepEthAmountToDrip);
} else {
// @> If the faucetClaimer is a non-payable contract (i.e., it has no receive() or fallback() functions), the entire claimFaucetTokens() function is rolled back
revert RaiseBoxFaucet_EthTransferFailed();
}

Risk

Likelihood:

  • The current logic excludes some contract users (such as decentralized applications, wallet proxy contracts, smart accounts, etc

  • ERC20 token redemption will be interrupted due to .call failure

Impact:

  • The entire claimFaucetTokens() function is rolled back.

  • All state changes (including ERC20 token issuance) are undone.

  • Non-payable contract addresses cannot receive any tokens.

Proof of Concept

// Non-payable contract, used to test ETH transfer failures
contract NonPayableContract {
// No receive() or fallback() functions, so ETH cannot be received
function someFunction() public pure returns (string memory) {
return "This contract cannot receive ETH";
}
}
// Test the issue of ERC20 issuance interruption caused by ETH transfer failure
function testEthTransferFailureInterruptsErc20Distribution() public {
// Deploy a non-payable contract
NonPayableContract nonPayableContract = new NonPayableContract();
address nonPayableAddress = address(nonPayableContract);
console.log("Non-payable contract address:", nonPayableAddress);
// Log the initial state of the contract
uint256 initialContractBalance = raiseBoxFaucet.getFaucetTotalSupply();
uint256 initialEthBalance = address(raiseBoxFaucet).balance;
uint256 initialNonPayableTokenBalance = raiseBoxFaucet.getBalance(nonPayableAddress);
uint256 initialNonPayableEthBalance = nonPayableAddress.balance;
console.log("Initial contract token balance:", initialContractBalance);
console.log("Initial contract ETH balance:", initialEthBalance);
console.log("Initial non-payable contract token balance:", initialNonPayableTokenBalance);
console.log("Initial non-payable contract ETH balance:", initialNonPayableEthBalance);
// Ensure the contract has enough ETH to attempt the transfer
assertTrue(initialEthBalance >= raiseBoxFaucet.sepEthAmountToDrip(), "Contract should have enough ETH");
// Attempt to have the non-payable contract call claimFaucetTokens
// This should fail, as the ETH transfer will fail, causing the entire transaction to roll back
vm.prank(nonPayableAddress);
// Expect the transaction to fail, as a failed ETH transfer will trigger the RaiseBoxFaucet_EthTransferFailed error
vm.expectRevert(RaiseBoxFaucet.RaiseBoxFaucet_EthTransferFailed.selector);
raiseBoxFaucet.claimFaucetTokens();
// Verify that the state has not changed, proving that the ERC20 token has not been issued
uint256 finalContractBalance = raiseBoxFaucet.getFaucetTotalSupply();
uint256 finalEthBalance = address(raiseBoxFaucet).balance;
uint256 finalNonPayableTokenBalance = raiseBoxFaucet.getBalance(nonPayableAddress);
uint256 finalNonPayableEthBalance = nonPayableAddress.balance;
console.log("Final contract token balance:", finalContractBalance);
console.log("Final contract ETH balance:", finalEthBalance);
console.log("Final non-payable contract token balance:", finalNonPayableTokenBalance);
console.log("Final non-payable contract ETH balance:", finalNonPayableEthBalance);
// Verify that all balances have not changed, proving the entire transaction has been rolled back
assertEq(finalContractBalance, initialContractBalance, "Contract token balances should not change");
assertEq(finalEthBalance, initialEthBalance, "Contract ETH balances should not change");
assertEq(finalNonPayableTokenBalance, initialNonPayableTokenBalance, "Non-payable contract token balances should not change");
assertEq(finalNonPayableEthBalance, initialNonPayableEthBalance, "Non-payable contract ETH balances should not change");
// Verify that the hasClaimedEth status has not changed
assertFalse(raiseBoxFaucet.getHasClaimedEth(nonPayableAddress), "hasClaimedEth status should not change");
console.log("Test completed: ETH The transfer failure successfully prevented the issuance of the ERC20 token");
}

Recommended Mitigation

//If ETH issuance fails, do not revert, skip it and continue issuing ERC20 tokens.
(bool success,) = faucetClaimer.call{value: sepEthAmountToDrip}("");
if (success) {
emit SepEthDripped(faucetClaimer, sepEthAmountToDrip);
} else {
emit SepEthDripSkipped(faucetClaimer, "ETH transfer failed (non-payable address)");
}
Updates

Lead Judging Commences

inallhonesty Lead Judge about 2 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!