Skip to content

Instantly share code, notes, and snippets.

@Philogy
Last active March 15, 2024 02:38
Show Gist options
  • Save Philogy/5a8e51433ece87904791422f728965b0 to your computer and use it in GitHub Desktop.
Save Philogy/5a8e51433ece87904791422f728965b0 to your computer and use it in GitHub Desktop.
Curta Golf Bounty Contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {ICurtaGolf} from "curta-golf/src/interfaces/ICurtaGolf.sol";
import {ICourse} from "curta-golf/src/interfaces/ICourse.sol";
import {IERC721} from "./interfaces/IERC721.sol";
import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol";
import {SafeCastLib} from "solady/utils/SafeCastLib.sol";
/// @author philogy <https://github.com/philogy>
contract GolfBounty {
using SafeTransferLib for address;
using SafeCastLib for uint256;
error NotExpired();
error Expired();
error NonexistentCourse();
error DoesntMeetThreshold();
error ThresholdAlreadyMet();
error AlreadyPaidOut();
error NothingToRefund();
event Deposit(Key indexed key, address indexed owner, uint256 amount);
event Refund(Key indexed key, address indexed owner, uint256 amount);
event Claimed(Key indexed key, address indexed to, uint256 total);
ICurtaGolf internal constant CURTA_GOLF = ICurtaGolf(0x8cCd70b1B74eA505dbA39d2D11C3aB6a2CB14A8c);
struct Key {
uint32 courseId;
uint32 threshold;
uint256 expiry;
}
struct Bounty {
bool paidOut;
uint128 total;
mapping(address => uint256) deposits;
}
mapping(uint32 courseId => mapping(uint32 threshold => mapping(uint256 expiry => Bounty))) internal _bounties;
function deposit(Key calldata key) external payable {
_checkNotExpired(key);
_checkCourseValidity(key);
Bounty storage bounty = _getBounty(key);
if (bounty.paidOut) revert AlreadyPaidOut();
bounty.total += msg.value.toUint128();
bounty.deposits[msg.sender] += msg.value;
emit Deposit(key, msg.sender, msg.value);
}
function refund(Key calldata key) external {
// Check that the bounty expired and wasn't empty.
if (key.expiry > block.timestamp) revert NotExpired();
Bounty storage bounty = _getBounty(key);
if (bounty.paidOut) revert AlreadyPaidOut();
uint256 amount = bounty.deposits[msg.sender];
// Reset and send caller their deposit.
bounty.deposits[msg.sender] = 0;
emit Refund(key, msg.sender, amount);
msg.sender.safeTransferETH(amount);
}
function claim(Key calldata key) external {
_checkNotExpired(key);
(, uint256 gasUsed,,) = CURTA_GOLF.getCourse(key.courseId);
if (gasUsed >= key.threshold) revert DoesntMeetThreshold();
// Send current holder of the KING token the total.
Bounty storage bounty = _getBounty(key);
if (bounty.paidOut) revert AlreadyPaidOut();
bounty.paidOut = true;
address to = IERC721(address(CURTA_GOLF)).ownerOf(key.courseId);
uint256 total = bounty.total;
emit Claimed(key, to, total);
to.safeTransferETH(total);
}
function getBounty(Key calldata key) external view returns (uint256, bool) {
Bounty storage bounty = _getBounty(key);
return (bounty.total, bounty.paidOut);
}
function getDeposit(Key calldata key, address owner) public view returns (uint256) {
return _getBounty(key).deposits[owner];
}
function _checkNotExpired(Key calldata key) internal view {
if (key.expiry <= block.timestamp) revert Expired();
}
function _checkCourseValidity(Key calldata key) internal view {
(ICourse setCourse, uint32 gasUsed,,) = CURTA_GOLF.getCourse(key.courseId);
if (address(setCourse) == address(0)) revert NonexistentCourse();
if (gasUsed < key.threshold) revert ThresholdAlreadyMet();
}
function _getBounty(Key calldata key) internal view returns (Bounty storage) {
return _bounties[key.courseId][key.threshold][key.expiry];
}
}
@Shungy
Copy link

Shungy commented Mar 14, 2024

gonna raise the med risk one to high cuz realized other depositors who got their bounty claimed can steal the new depositor by calling refund.

A deposits 50
B solves and claims 50 from A
C coincidentally deposits 100 right after bounty was claimed.
A refunds, stealing from C 50

@Shungy
Copy link

Shungy commented Mar 15, 2024

New version doesn't fully fix the main issue. Because the newly added bounty.paidOut checks in refund() and claim() not only prevent stealing late depositor funds, they also lock the funds which get deposited after a claim. So it should simply prevent depositing in after a claim. To be extra safe, I would prevent depositing when threshold is already met regardless of bounty being claimed or not. I suggest the fix below.

diff --git a/GolfBounty.sol b/GolfBounty-update.sol
index 3a1b575..6c08da9 100644
--- a/GolfBounty.sol
+++ b/GolfBounty-update.sol
@@ -16,6 +16,7 @@ contract GolfBounty {
     error Expired();
     error NonexistentCourse();
     error DoesntMeetThreshold();
+    error ThresholdAlreadyMet();
     error AlreadyPaidOut();
     error NothingToRefund();
 
@@ -41,7 +42,7 @@ contract GolfBounty {
 
     function deposit(Key calldata key) external payable {
         _checkNotExpired(key);
-        _checkExistingCourse(key);
+        _checkCourseValidity(key);
         Bounty storage bounty = _getBounty(key);
         bounty.total += msg.value.toUint128();
         bounty.deposits[msg.sender] += msg.value;
@@ -87,9 +88,10 @@ contract GolfBounty {
         if (key.expiry <= block.timestamp) revert Expired();
     }
 
-    function _checkExistingCourse(Key calldata key) internal view {
-        (ICourse setCourse,,,) = CURTA_GOLF.getCourse(key.courseId);
+    function _checkCourseValidity(Key calldata key) internal view {
+        (ICourse setCourse, uint256 gasUsed,,) = CURTA_GOLF.getCourse(key.courseId);
         if (address(setCourse) == address(0)) revert NonexistentCourse();
+        if (gasUsed < key.threshold) revert ThresholdAlreadyMet();
     }
 
     function _getBounty(Key calldata key) internal view returns (Bounty storage) {

Other than this LGTM.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment