-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
8bde6dd
commit 6196121
Showing
19 changed files
with
1,117 additions
and
57 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.28; | ||
|
||
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; | ||
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; | ||
import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; | ||
import {ERC20} from "../lib/solmate/src/tokens/ERC20.sol"; | ||
import {SafeTransferLib} from "../lib/solmate/src/utils/SafeTransferLib.sol"; | ||
import {IChallengeManager} from "./interfaces/IChallengeManager.sol"; | ||
import {IChallenge} from "./interfaces/IChallenge.sol"; | ||
import {Types} from "./libraries/Types.sol"; | ||
import {ErrorsLib} from "./libraries/ErrorsLib.sol"; | ||
import {TimeLib} from "./libraries/TimeLib.sol"; | ||
import {DripVault} from "./DripVault.sol"; | ||
|
||
contract Challenge is ERC721, IChallenge { | ||
using Math for uint256; | ||
using SafeTransferLib for ERC20; | ||
|
||
uint256 private _tokenId; // Token ID for each challenge; starts at 0 | ||
address public profileContract; | ||
address public managerContract; | ||
|
||
mapping(uint256 => Types.Challenge) private _challenges; // tokenId => challenge | ||
|
||
constructor() ERC721("Drip Challenge", "DCH") {} | ||
|
||
modifier onlyProfile(address profileOwner) { | ||
// Only the profile contract or the owner can call this function | ||
require(msg.sender == profileContract || msg.sender == profileOwner, ErrorsLib.NOT_OWNER); | ||
_; | ||
} | ||
|
||
function setContracts(address _profileContract, address _managerContract) external { | ||
// Can only be set once | ||
require(profileContract == address(0), ErrorsLib.PROFILE_CONTRACT_ALREADY_SET); | ||
profileContract = _profileContract; | ||
managerContract = _managerContract; | ||
} | ||
|
||
function createChallenge(Types.CreateChallengeParams calldata params) | ||
external | ||
onlyProfile(params.owner) | ||
returns (uint256 challengeId) | ||
{ | ||
require(params.durationInDays <= 365, ErrorsLib.INVALID_DURATION); | ||
|
||
uint256 epochId = IChallengeManager(managerContract).getCurrentEpochId(); | ||
(Types.Epoch memory epoch, Types.EpochStatus status) = IChallengeManager(managerContract).getEpochInfo(epochId); | ||
require(status == Types.EpochStatus.Active, ErrorsLib.INVALID_EPOCH_STATUS); | ||
|
||
// Transfer epoch asset from user to challenge manager | ||
ERC20(epoch.asset).safeTransferFrom(params.owner, managerContract, params.depositAmount); | ||
|
||
// Calculate duration and end time | ||
uint256 startTime = block.timestamp; | ||
uint256 endTime = TimeLib.addDaysToTimestamp(startTime, params.durationInDays); | ||
|
||
Types.Challenge storage challenge = _challenges[_tokenId]; | ||
challenge.id = _tokenId; | ||
challenge.title = params.title; | ||
challenge.description = params.description; | ||
challenge.owner = params.owner; | ||
challenge.startTime = startTime; | ||
challenge.endTime = endTime; | ||
challenge.depositAmount = params.depositAmount; | ||
challenge.depositToken = address(0); // TODO: Get from epoch | ||
challenge.durationInDays = params.durationInDays; | ||
challenge.epochId = params.epochId; | ||
challenge.dailyCompletionTimestamps = new uint256[](params.durationInDays); | ||
|
||
// Mint and emit the challenge event | ||
_safeMint(params.owner, _tokenId); | ||
emit ChallengeCreated(_tokenId, params.owner); | ||
|
||
// Add the challenge to the epoch | ||
IChallengeManager(managerContract).addChallengeToEpoch( | ||
params.epochId, _tokenId, params.owner, params.depositAmount | ||
); | ||
|
||
// Return challenge ID and increment the token counter | ||
challengeId = _tokenId; | ||
++_tokenId; | ||
} | ||
|
||
function getChallenge(uint256 challengeId) external view returns (Types.Challenge memory) { | ||
return _challenges[challengeId]; | ||
} | ||
|
||
function getChallengesByEpoch(uint256 epochId) external view returns (Types.Challenge[] memory) { | ||
uint256[] memory challengeIds = IChallengeManager(managerContract).getEpochChallenges(epochId); | ||
uint256 length = challengeIds.length; | ||
Types.Challenge[] memory challenges = new Types.Challenge[](length); | ||
|
||
for (uint256 i = 0; i < length;) { | ||
challenges[i] = _challenges[challengeIds[i]]; | ||
unchecked { | ||
++i; | ||
} | ||
} | ||
return challenges; | ||
} | ||
|
||
function submitDailyCompletion(address owner, uint256 tokenId, uint16 day) external onlyProfile(owner) { | ||
Types.Challenge storage challenge = _challenges[tokenId]; | ||
require(day < challenge.durationInDays, ErrorsLib.EXCEED_DURATION); | ||
require(challenge.dailyCompletionTimestamps[day] == 0, ErrorsLib.ALREADY_COMPLETED); | ||
|
||
uint256 dayStartTime = TimeLib.addDaysToTimestamp(challenge.startTime, day); | ||
uint256 dayEndTime = TimeLib.addDaysToTimestamp(dayStartTime, 1); | ||
|
||
require(block.timestamp >= dayStartTime && block.timestamp <= dayEndTime, ErrorsLib.NOT_IN_TIME_RANGE); | ||
|
||
challenge.dailyCompletionTimestamps[day] = block.timestamp; | ||
emit DailyCompletionSubmitted(tokenId, day); | ||
} | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.26; | ||
|
||
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; | ||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | ||
import {IERC4626} from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; | ||
import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; | ||
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; | ||
import {IChallengeManager} from "./interfaces/IChallengeManager.sol"; | ||
import {IChallenge} from "./interfaces/IChallenge.sol"; | ||
import {ErrorsLib} from "./libraries/ErrorsLib.sol"; | ||
import {TimeLib} from "./libraries/TimeLib.sol"; | ||
import {Types} from "./libraries/Types.sol"; | ||
import {DripVault} from "./DripVault.sol"; | ||
|
||
contract ChallengeManager is Ownable, IChallengeManager { | ||
using Math for uint256; | ||
using EnumerableSet for EnumerableSet.AddressSet; | ||
|
||
IChallenge public challenge; | ||
|
||
// keccak256(abi.encode(uint256(keccak256("drip.ChallengeManager")) - 1)) & ~bytes32(uint256(0xff)); | ||
bytes32 private constant ChallengeManagerStorageLocation = | ||
0x2e5d4946837baa00c2caecc5c5227a1c2b19f4bab88cdee66eaa65badd325500; | ||
|
||
modifier onlyChallenge() { | ||
require(msg.sender == address(challenge), ErrorsLib.NOT_AUTHORIZED); | ||
_; | ||
} | ||
|
||
constructor(address _challenge) Ownable(msg.sender) { | ||
challenge = IChallenge(_challenge); | ||
} | ||
|
||
/// @notice Returns the current epoch ID. | ||
/// @return ID of the current epoch. | ||
function getCurrentEpochId() external view returns (uint256) { | ||
return _getChallengeManagerStorage().currentEpoch.id; | ||
} | ||
|
||
/// @notice Returns the information of an epoch. | ||
/// @param epochId The ID of the epoch. | ||
/// @return Information of the epoch. | ||
function getEpochInfo(uint256 epochId) external view returns (Types.Epoch memory, Types.EpochStatus) { | ||
Types.ChallengeManagerStorage storage $ = _getChallengeManagerStorage(); | ||
return ($.epochs[epochId], getEpochStatus(epochId)); | ||
} | ||
|
||
/// @notice Returns the status of an epoch. | ||
/// @param epochId The ID of the epoch. | ||
/// @return Status of the epoch. | ||
function getEpochStatus(uint256 epochId) public view returns (Types.EpochStatus) { | ||
Types.Epoch memory epoch = _getChallengeManagerStorage().epochs[epochId]; | ||
if (block.timestamp < epoch.startTimestamp) { | ||
return Types.EpochStatus.Pending; | ||
} else if (block.timestamp < epoch.endTimestamp) { | ||
return Types.EpochStatus.Active; | ||
} else if (block.timestamp < epoch.endTimestamp + 7 days) { | ||
return Types.EpochStatus.Ended; | ||
} else { | ||
return Types.EpochStatus.Closed; | ||
} | ||
} | ||
|
||
/// @notice Starts a new epoch. | ||
/// @param description The description. | ||
/// @param start The start timestamp. | ||
/// @param durationInDays The duration of the epoch in days. | ||
/// @param asset The asset to be deposited in the epoch. | ||
/// @return id of the new epoch. | ||
function startEpoch(string calldata description, uint256 start, uint16 durationInDays, address asset) | ||
external | ||
onlyOwner | ||
returns (uint256 id) | ||
{ | ||
require(asset != address(0), ErrorsLib.ZERO_ADDRESS); | ||
Types.ChallengeManagerStorage storage $ = _getChallengeManagerStorage(); | ||
require(getEpochStatus($.currentEpoch.id) >= Types.EpochStatus.Ended, ErrorsLib.INVALID_EPOCH_STATUS); | ||
|
||
id = $.nextEpochId; | ||
|
||
uint256 endTimestamp = TimeLib.addDaysToTimestamp(start, durationInDays); | ||
|
||
DripVault vault = new DripVault(asset); | ||
|
||
Types.Epoch memory epoch = Types.Epoch({ | ||
id: id, | ||
description: description, | ||
durationInDays: durationInDays, | ||
startTimestamp: start, | ||
endTimestamp: endTimestamp, | ||
vault: address(vault), | ||
asset: asset, | ||
participantCount: 0, | ||
totalDeposits: 0 | ||
}); | ||
$.epochs[id] = epoch; | ||
$.currentEpoch = epoch; | ||
$.nextEpochId++; | ||
emit EpochStarted(id, start, endTimestamp); | ||
} | ||
|
||
function addChallengeToEpoch(uint256 epochId, uint256 challengeId, address challengeOwner, uint256 depositAmount) | ||
external | ||
onlyChallenge | ||
{ | ||
Types.ChallengeManagerStorage storage $ = _getChallengeManagerStorage(); | ||
IERC20($.epochs[epochId].asset).approve($.epochs[epochId].vault, type(uint256).max); | ||
IERC4626($.epochs[epochId].vault).deposit(depositAmount, challengeOwner); | ||
|
||
$.epochChallenges[epochId].push(challengeId); | ||
EnumerableSet.AddressSet storage participants = $.epochParticipants[epochId]; | ||
participants.add(challengeOwner); | ||
// Update epoch info | ||
$.epochs[epochId].totalDeposits = IERC4626($.epochs[epochId].vault).totalAssets(); | ||
$.epochs[epochId].participantCount = participants.length(); | ||
} | ||
|
||
function getEpochChallenges(uint256 epochId) external view returns (uint256[] memory) { | ||
return _getChallengeManagerStorage().epochChallenges[epochId]; | ||
} | ||
|
||
/// @notice Returns whether a token is whitelisted. | ||
/// @notice This is not used yet. | ||
/// @param token The address of the token to check. | ||
/// @return Whether the token is whitelisted. | ||
function isWhiteListedToken(address token) external view returns (bool) { | ||
return _getChallengeManagerStorage().isWhitelistedToken[token]; | ||
} | ||
|
||
/// @notice DEMO ONLY | ||
/// @notice Sets the epoch. | ||
function setEpoch(uint256 epochId, Types.Epoch memory epoch) external onlyOwner { | ||
Types.ChallengeManagerStorage storage $ = _getChallengeManagerStorage(); | ||
$.epochs[epochId] = epoch; | ||
$.currentEpoch = epoch; | ||
$.nextEpochId++; | ||
} | ||
|
||
/// @dev Returns the storage struct of ChallengeManager. | ||
function _getChallengeManagerStorage() private pure returns (Types.ChallengeManagerStorage storage $) { | ||
assembly { | ||
$.slot := ChallengeManagerStorageLocation | ||
} | ||
} | ||
} |
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.