Skip to content

Commit

Permalink
feat: update project
Browse files Browse the repository at this point in the history
  • Loading branch information
Doge-is-Dope committed Nov 15, 2024
1 parent 8bde6dd commit 6196121
Show file tree
Hide file tree
Showing 19 changed files with 1,117 additions and 57 deletions.
19 changes: 0 additions & 19 deletions script/Counter.s.sol

This file was deleted.

117 changes: 117 additions & 0 deletions src/Challenge.sol
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);
}
}
146 changes: 146 additions & 0 deletions src/ChallengeManager.sol
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
}
}
}
14 changes: 0 additions & 14 deletions src/Counter.sol

This file was deleted.

Loading

0 comments on commit 6196121

Please sign in to comment.