Last active
March 15, 2024 02:38
-
-
Save Philogy/5a8e51433ece87904791422f728965b0 to your computer and use it in GitHub Desktop.
Curta Golf Bounty Contract
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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]; | |
} | |
} |
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
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