diff --git a/script/BaseDeploy.s.sol b/script/BaseDeploy.s.sol index d7d9b18..bfe0bcb 100644 --- a/script/BaseDeploy.s.sol +++ b/script/BaseDeploy.s.sol @@ -14,6 +14,6 @@ contract BaseDeploy is Script { } function _calculateSalt(string memory input) internal pure returns (bytes32) { - return keccak256(abi.encodePacked(input)) & ~bytes32(uint256(0xffff)); + return keccak256(abi.encodePacked(input)) & ~bytes32(uint256(0xffff) - 4); } } diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index 6de6071..dc5badb 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -23,15 +23,94 @@ contract Deploy is BaseDeploy { Challenge challenge; ChallengeManager challengeManager; DripProfile profile; - + address vault0; + address vault1; // Profiles mapping(address => Types.Profile) public profiles; function run() public { _deployContracts(); + + // DEMO ONLY + _initializeEpochs(); + _createProfiles(); + + // Create claimable challenges + uint256[] memory timestamps1 = new uint256[](5); + timestamps1[0] = 1; + timestamps1[1] = 0; + timestamps1[2] = 0; + timestamps1[3] = 0; + timestamps1[4] = 1; + _createClaimableChallenge(1, 0, timestamps1, 100e18, deployerPrivateKey); + uint256[] memory timestamps2 = new uint256[](5); + timestamps2[0] = 1; + timestamps2[1] = 1; + timestamps2[2] = 1; + timestamps2[3] = 1; + timestamps2[4] = 1; + _createClaimableChallenge(2, 0, timestamps2, 100e18, ShanePrivateKey); + + // Deployer calculates claimable + _calculateClaimable(vault0); + + // Create active challenges + // 0/5 + uint256[] memory ts1 = new uint256[](5); + ts1[0] = 0; + ts1[1] = 0; + ts1[2] = 0; + ts1[3] = 0; + ts1[4] = 0; + _createActiveChallenge(2, 1, "7 Days Quest", "Daily quests for one week", ts1, 101e18, ShanePrivateKey); + + // 1/5 + uint256[] memory ts2 = new uint256[](5); + ts2[0] = 0; + ts2[1] = 0; + ts2[2] = 1; + ts2[3] = 0; + ts2[4] = 0; + _createActiveChallenge(2, 1, "Challenge", "Demo challenge", ts2, 10e18, ShanePrivateKey); + + // 2/5 + uint256[] memory ts3 = new uint256[](5); + ts3[0] = 0; + ts3[1] = 0; + ts3[2] = 1; + ts3[3] = 0; + ts3[4] = 1; + _createActiveChallenge(2, 1, "Happy Challenge", "Practice makes perfect", ts3, 5e18, ShanePrivateKey); + + // 3/5 + uint256[] memory ts4 = new uint256[](5); + ts4[0] = 0; + ts4[1] = 1; + ts4[2] = 1; + ts4[3] = 1; + ts4[4] = 0; + _createActiveChallenge(2, 1, "Better myself", "I luv Challenge", ts4, 12e18, ShanePrivateKey); + + // 5/5 + uint256[] memory ts5 = new uint256[](5); + ts5[0] = 1; + ts5[1] = 1; + ts5[2] = 1; + ts5[3] = 1; + ts5[4] = 1; + _createActiveChallenge(2, 1, "New Begin", "Whole new me", ts4, 100e18, ShanePrivateKey); + + // 4/5 + uint256[] memory ts6 = new uint256[](5); + ts6[0] = 1; + ts6[1] = 1; + ts6[2] = 1; + ts6[3] = 1; + ts6[4] = 0; + _createActiveChallenge(2, 1, "Nailed it", "I'm a pro", ts6, 100e18, ShanePrivateKey); } - function _deployContracts() private broadcast(deployerPrivateKey) { + function _deployContracts() private { // Deploy Challenge bytes memory challengeBytecode = abi.encodePacked(type(Challenge).creationCode); challenge = Challenge(_create2Deploy("Drip.Challenge.ff", challengeBytecode)); @@ -50,8 +129,104 @@ contract Deploy is BaseDeploy { console.log("DripProfile deployed to", address(profile)); } - function _create2Deploy(string memory fileName, bytes memory creationBytecode) private returns (address) { + function _create2Deploy(string memory fileName, bytes memory creationBytecode) + private + broadcast(deployerPrivateKey) + returns (address) + { bytes32 salt = _calculateSalt(fileName); return Create2.deploy(0, salt, creationBytecode); } + + function _initializeEpochs() private { + uint256 currentTimestamp = block.timestamp; + vault0 = _createEpoch(0, currentTimestamp - 5 days, "ETHGlobal Bangkok Demo"); + vault1 = _createEpoch(1, currentTimestamp - 4 days, "TOEFL 7000 words"); + console.log("Epochs initialized"); + } + + /// @notice DEMO only - Helper function to create an epoch + function _createEpoch(uint256 id, uint256 startTimestamp, string memory description) + private + broadcast(deployerPrivateKey) + returns (address) + { + DripVault vault = new DripVault(MOCK_TOKEN, address(challengeManager)); + Types.Epoch memory epoch = Types.Epoch({ + id: id, + startTimestamp: startTimestamp, + endTimestamp: startTimestamp + DURATION, + durationInDays: DURATION.convertSecondsToDays(), + vault: address(vault), + asset: MOCK_TOKEN, + description: description, + participantCount: 0, + totalDeposits: 0 + }); + challengeManager.setEpoch(id, epoch); + return address(vault); + } + + /// @notice DEMO only - Helper function to create profiles + function _createProfiles() private { + uint32[] memory avatar1 = new uint32[](5); + avatar1[0] = uint32(0); + avatar1[1] = uint32(0); + avatar1[2] = uint32(0); + avatar1[3] = uint32(0); + avatar1[4] = uint32(0); + _createProfile("@clement", avatar1, vm.envUint("PRIVATE_KEY")); + + uint32[] memory avatar2 = new uint32[](5); + avatar2[0] = uint32(1); + avatar2[1] = uint32(1); + avatar2[2] = uint32(1); + avatar2[3] = uint32(1); + avatar2[4] = uint32(1); + _createProfile("@shane", avatar2, vm.envUint("PRIVATE_KEY_SHA")); + } + + function _createProfile(string memory handle, uint32[] memory avatar, uint256 pk) private broadcast(pk) { + address account = vm.addr(pk); + uint256 profileId = profile.createProfile(account, handle, avatar); + Types.Profile memory createdProfile = profile.getProfileById(profileId); + profiles[account] = createdProfile; + console.log(handle, "profile with ID:", profileId); + + // Fund profile with mock token + ERC20Mock(MOCK_TOKEN).setBalance(account, 1_000_000e18); + ERC20Mock(MOCK_TOKEN).approve(address(challenge), type(uint256).max); + } + + function _calculateClaimable(address vault) private broadcast(deployerPrivateKey) { + DripVault(vault).calculateOwnersClaimable(); + } + + function _createClaimableChallenge( + uint256 profileId, + uint256 epochId, + uint256[] memory timestamps, + uint256 amount, + uint256 pk + ) private broadcast(pk) { + uint256 current = block.timestamp; + uint256 challengeId = + profile.createChallengeDemo(profileId, epochId, "Title", "desc", current - 388800, amount, 5); + profile.setDailyCompletionDemo(challengeId, timestamps); + } + + function _createActiveChallenge( + uint256 profileId, + uint256 epochId, + string memory title, + string memory description, + uint256[] memory timestamps, + uint256 amount, + uint256 pk + ) private broadcast(pk) { + uint256 current = block.timestamp; + uint256 challengeId = + profile.createChallengeDemo(profileId, epochId, title, description, current - 4 days, amount, 5); + profile.setDailyCompletionDemo(challengeId, timestamps); + } } diff --git a/src/Challenge.sol b/src/Challenge.sol index 8ee5ee1..c62dcce 100644 --- a/src/Challenge.sol +++ b/src/Challenge.sol @@ -6,6 +6,7 @@ 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 {ChallengeManager} from "./ChallengeManager.sol"; import {IChallengeManager} from "./interfaces/IChallengeManager.sol"; import {IChallenge} from "./interfaces/IChallenge.sol"; import {Types} from "./libraries/Types.sol"; @@ -118,4 +119,57 @@ contract Challenge is ERC721, IChallenge { emit DailyCompletionSubmitted(tokenId, day); } + + /// @dev Only for demo + function createChallengeDemo(Types.CreateChallengeParams calldata params, uint256 epochId, uint256 startTime) + external + onlyProfile(params.owner) + returns (uint256 challengeId) + { + (Types.Epoch memory epoch,) = IChallengeManager(managerContract).getEpochInfo(epochId); + + // Transfer epoch asset from user to challenge manager + ERC20(epoch.asset).safeTransferFrom(params.owner, managerContract, params.depositAmount); + + // Calculate duration and end time + 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 + ChallengeManager(managerContract).addChallengeToEpochDemo(params.epochId, challenge); + + // Return challenge ID and increment the token counter + challengeId = _tokenId; + ++_tokenId; + } + + function setDailyCompletionDemo(address owner, uint256 tokenId, uint256[] memory timestamps) + external + onlyProfile(owner) + { + Types.Challenge storage challenge = _challenges[tokenId]; + + challenge.dailyCompletionTimestamps = timestamps; + + // Update vault + (Types.Epoch memory epoch,) = IChallengeManager(managerContract).getEpochInfo(challenge.epochId); + DripVault vault = DripVault(epoch.vault); + vault.submitDailyCompletion(owner, challenge.dailyCompletionTimestamps); + } } diff --git a/src/ChallengeManager.sol b/src/ChallengeManager.sol index 589cf31..1cf90d2 100644 --- a/src/ChallengeManager.sol +++ b/src/ChallengeManager.sol @@ -126,15 +126,29 @@ contract ChallengeManager is Ownable, IChallengeManager { return _getChallengeManagerStorage().isWhitelistedToken[token]; } - /// @notice DEMO ONLY - /// @notice Sets the epoch. - function setEpoch(uint256 epochId, Types.Epoch memory epoch) external onlyOwner { + /// @dev DEMO ONLY + function setEpoch(uint256 epochId, Types.Epoch memory epoch) external { Types.ChallengeManagerStorage storage $ = _getChallengeManagerStorage(); $.epochs[epochId] = epoch; $.currentEpoch = epoch; $.nextEpochId++; } + /// @dev DEMO ONLY + function addChallengeToEpochDemo(uint256 epochId, Types.Challenge calldata userChallenge) external onlyChallenge { + Types.ChallengeManagerStorage storage $ = _getChallengeManagerStorage(); + IERC20($.epochs[epochId].asset).approve($.epochs[epochId].vault, type(uint256).max); + IERC4626($.epochs[epochId].vault).deposit(userChallenge.depositAmount, userChallenge.owner); + DripVault($.epochs[epochId].vault).setOwnerChallengeDays(userChallenge.owner, userChallenge.durationInDays); + + $.epochChallenges[epochId].push(userChallenge.id); + EnumerableSet.AddressSet storage participants = $.epochParticipants[epochId]; + participants.add(userChallenge.owner); + // Update epoch info + $.epochs[epochId].totalDeposits = IERC4626($.epochs[epochId].vault).totalAssets(); + $.epochs[epochId].participantCount = participants.length(); + } + /** * @notice Preview rewards in an epoch */ diff --git a/src/DripProfile.sol b/src/DripProfile.sol index 73d9a55..07e82b3 100644 --- a/src/DripProfile.sol +++ b/src/DripProfile.sol @@ -112,6 +112,24 @@ contract DripProfile is ERC721, IDripProfile { return challengeId; } + /// @dev Only for demo + function createChallengeDemo( + uint256 profileId, + uint256 epochId, + string calldata name, + string calldata description, + uint256 startTime, + uint256 depositAmount, + uint16 durationInDays + ) external onlyProfileOwner(profileId) returns (uint256) { + require(profileId != 0, ErrorsLib.PROFILE_NOT_EXISTS); + Types.CreateChallengeParams memory params = + Types.CreateChallengeParams(msg.sender, depositAmount, epochId, durationInDays, name, description); + uint256 challengeId = challenge.createChallengeDemo(params, epochId, startTime); + _profileIdToEpochIdToChallengeIds[profileId][epochId].push(challengeId); + return challengeId; + } + /** * @dev Retrieves the challenges associated with a profile. * @param profileId The ID of the profile to retrieve the challenges. @@ -148,6 +166,11 @@ contract DripProfile is ERC721, IDripProfile { challenge.submitDailyCompletion(msg.sender, challengeId, day); } + /// @dev Only for demo + function setDailyCompletionDemo(uint256 challengeId, uint256[] memory timestamps) external { + challenge.setDailyCompletionDemo(msg.sender, challengeId, timestamps); + } + function _update(address to, uint256 tokenId, address auth) internal override returns (address) { address from = super._update(to, tokenId, auth); require(from == address(0), ErrorsLib.NOT_TRANSFERABLE);