diff --git a/challenge-1-vesting/README.md b/challenge-1-vesting/README.md index 9cc7a2c..9372416 100644 --- a/challenge-1-vesting/README.md +++ b/challenge-1-vesting/README.md @@ -8,9 +8,9 @@ OpenGuild Labs makes the repository to introduce OpenHack workshop participants Add your information to the below list to officially participate in the workshop challenge (This is the first mission of the whole workshop) -| Emoji | Name | Github Username | Occupations | -| ----- | ---- | ------------------------------------- | ----------- | -| 🎅 | Ippo | [NTP-996](https://github.com/NTP-996) | DevRel | +| Emoji | Name | Github Username | Occupations | +| ----- | ---- | ------------------------------------- | ----------- | +| 👨🏻‍💻 | Toheeb | [Horlarmmy](https://github.com/Horlarmmy) | Software Engineer | ## 💻 Local development environment setup diff --git a/challenge-1-vesting/contracts/TokenVesting.sol b/challenge-1-vesting/contracts/TokenVesting.sol index 43d4c3a..f8a81c9 100644 --- a/challenge-1-vesting/contracts/TokenVesting.sol +++ b/challenge-1-vesting/contracts/TokenVesting.sol @@ -31,17 +31,25 @@ import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; contract TokenVesting is Ownable(msg.sender), Pausable, ReentrancyGuard { struct VestingSchedule { // TODO: Define the vesting schedule struct + uint256 totalAmount; // Total tokens to be vested + uint256 startTime; // Vesting start time + uint256 cliffDuration; // Cliff duration in seconds + uint256 vestingDuration; // Total vesting duration in seconds + uint256 amountClaimed; // Amount of tokens already claimed + bool revoked; // Whether the vesting schedule is revoked } // Token being vested // TODO: Add state variables - + IERC20 public immutable token; // Mapping from beneficiary to vesting schedule // TODO: Add state variables + mapping(address => VestingSchedule) public vestingSchedules; // Whitelist of beneficiaries // TODO: Add state variables + mapping(address => bool) public whitelist; // Events event VestingScheduleCreated(address indexed beneficiary, uint256 amount); @@ -51,8 +59,9 @@ contract TokenVesting is Ownable(msg.sender), Pausable, ReentrancyGuard { event BeneficiaryRemovedFromWhitelist(address indexed beneficiary); constructor(address tokenAddress) { - // TODO: Initialize the contract - + // TODO: Initialize the contract + require(tokenAddress != address(0), "Invalid token address"); + token = IERC20(tokenAddress); } // Modifier to check if beneficiary is whitelisted @@ -80,21 +89,79 @@ contract TokenVesting is Ownable(msg.sender), Pausable, ReentrancyGuard { uint256 startTime ) external onlyOwner onlyWhitelisted(beneficiary) whenNotPaused { // TODO: Implement vesting schedule creation + require(beneficiary != address(0), "Invalid beneficiary address"); + require(amount > 0, "Amount must be greater than 0"); + require(vestingDuration > cliffDuration, "Vesting duration must be greater than cliff"); + + VestingSchedule storage schedule = vestingSchedules[beneficiary]; + require(schedule.totalAmount == 0, "Vesting schedule already exists"); + + vestingSchedules[beneficiary] = VestingSchedule({ + totalAmount: amount, + startTime: startTime, + cliffDuration: cliffDuration, + vestingDuration: vestingDuration, + amountClaimed: 0, + revoked: false + }); + + require(token.transferFrom(msg.sender, address(this), amount), "Token transfer failed"); + + emit VestingScheduleCreated(beneficiary, amount); } function calculateVestedAmount( address beneficiary ) public view returns (uint256) { // TODO: Implement vested amount calculation + VestingSchedule storage schedule = vestingSchedules[beneficiary]; + uint256 currentTime = block.timestamp; + + if (currentTime < schedule.startTime + schedule.cliffDuration || schedule.revoked) { + return 0; + } + + uint256 elapsedTime = currentTime - schedule.startTime; + uint256 totalVestingTime = schedule.vestingDuration; + + if (elapsedTime >= totalVestingTime) { + return schedule.totalAmount - schedule.amountClaimed; + } + + uint256 vestedAmount = (schedule.totalAmount * elapsedTime) / totalVestingTime; + return vestedAmount - schedule.amountClaimed; } function claimVestedTokens() external nonReentrant whenNotPaused { - // TODO: Implement token claiming + // TODO: Implement token claiming + VestingSchedule storage schedule = vestingSchedules[msg.sender]; + require(schedule.totalAmount > 0, "No vesting schedule found"); + require(!schedule.revoked, "Vesting schedule revoked"); + + uint256 claimableAmount = calculateVestedAmount(msg.sender); + require(claimableAmount > 0, "No tokens to claim"); + + schedule.amountClaimed += claimableAmount; + require(token.transfer(msg.sender, claimableAmount), "Token transfer failed"); + + emit TokensClaimed(msg.sender, claimableAmount); } function revokeVesting(address beneficiary) external onlyOwner { // TODO: Implement vesting revocation + VestingSchedule storage schedule = vestingSchedules[beneficiary]; + require(schedule.totalAmount > 0, "No vesting schedule found"); + require(!schedule.revoked, "Vesting schedule already revoked"); + + uint256 vestedAmount = calculateVestedAmount(beneficiary); + uint256 unvestedAmount = schedule.totalAmount - schedule.amountClaimed - vestedAmount; + schedule.revoked = true; + if (unvestedAmount > 0) { + require(token.transfer(msg.sender, unvestedAmount), "Token transfer failed"); + } + + emit VestingRevoked(beneficiary); } function pause() external onlyOwner { @@ -145,4 +212,4 @@ Solution template (key points to implement): - Calculate and transfer unvested tokens back - Mark schedule as revoked - Emit event -*/ \ No newline at end of file +*/ diff --git a/challenge-2-yield-farm/contracts/yeild.sol b/challenge-2-yield-farm/contracts/yeild.sol index 421496a..1611a42 100644 --- a/challenge-2-yield-farm/contracts/yeild.sol +++ b/challenge-2-yield-farm/contracts/yeild.sol @@ -71,6 +71,15 @@ contract YieldFarm is ReentrancyGuard, Ownable { uint256 _rewardRate ) Ownable(msg.sender) { // TODO: Initialize contract state + + require(_lpToken != address(0), "Invalid LP token address"); + require(_rewardToken != address(0), "Invalid reward token address"); + require(_rewardRate > 0, "Reward rate must be greater than 0"); + + lpToken = IERC20(_lpToken); + rewardToken = IERC20(_rewardToken); + rewardRate = _rewardRate; + lastUpdateTime = block.timestamp; } function updateReward(address _user) internal { @@ -90,6 +99,12 @@ contract YieldFarm is ReentrancyGuard, Ownable { // 1. Calculate rewards since last update // 2. Apply boost multiplier // 3. Return total pending rewards + + if (totalStaked == 0) { + return rewardPerTokenStored; + } + uint256 timeElapsed = block.timestamp - lastUpdateTime; + return rewardPerTokenStored + (timeElapsed * rewardRate * 1e18) / totalStaked; } function earned(address _user) public view returns (uint256) { @@ -98,6 +113,11 @@ contract YieldFarm is ReentrancyGuard, Ownable { // 1. Calculate rewards since last update // 2. Apply boost multiplier // 3. Return total pending rewards + + UserInfo storage user = userInfo[_user]; + uint256 boostRewardPerToken = rewardPerToken() * calculateBoostMultiplier(_user) / 100; + uint256 userReward = (user.amount * boostRewardPerToken) / 1e18; + return userReward - user.rewardDebt + user.pendingRewards; } /** @@ -111,6 +131,21 @@ contract YieldFarm is ReentrancyGuard, Ownable { // 2. Transfer LP tokens from user // 3. Update user info and total staked amount // 4. Emit Staked event + + require(_amount > 0, "Cannot stake 0"); + + updateReward(msg.sender); + + UserInfo storage user = userInfo[msg.sender]; + if (user.amount == 0) { + user.startTime = block.timestamp; + } + user.amount += _amount; + totalStaked += _amount; + + lpToken.transferFrom(msg.sender, address(this), _amount); + + emit Staked(msg.sender, _amount); } /** @@ -124,6 +159,18 @@ contract YieldFarm is ReentrancyGuard, Ownable { // 2. Transfer LP tokens to user // 3. Update user info and total staked amount // 4. Emit Withdrawn event + + UserInfo storage user = userInfo[msg.sender]; + require(user.amount >= _amount, "Insufficient balance"); + + updateReward(msg.sender); + + user.amount -= _amount; + totalStaked -= _amount; + + lpToken.transfer(msg.sender, _amount); + + emit Withdrawn(msg.sender, _amount); } /** @@ -136,6 +183,17 @@ contract YieldFarm is ReentrancyGuard, Ownable { // 2. Transfer rewards to user // 3. Update user reward debt // 4. Emit RewardsClaimed event + + updateReward(msg.sender); + + UserInfo storage user = userInfo[msg.sender]; + uint256 rewards = user.pendingRewards; + require(rewards > 0, "No rewards to claim"); + + user.pendingRewards = 0; + rewardToken.transfer(msg.sender, rewards); + + emit RewardsClaimed(msg.sender, rewards); } /** @@ -147,6 +205,19 @@ contract YieldFarm is ReentrancyGuard, Ownable { // 1. Transfer all LP tokens back to user // 2. Reset user info // 3. Emit EmergencyWithdrawn event + + UserInfo storage user = userInfo[msg.sender]; + uint256 amount = user.amount; + + require(amount > 0, "Nothing to withdraw"); + + user.amount = 0; + user.pendingRewards = 0; + totalStaked -= amount; + + lpToken.transfer(msg.sender, amount); + + emit EmergencyWithdrawn(msg.sender, amount); } /** @@ -161,6 +232,14 @@ contract YieldFarm is ReentrancyGuard, Ownable { // Requirements: // 1. Calculate staking duration // 2. Return appropriate multiplier based on duration thresholds + + require(_user != address(0), "Invalid user address"); + uint256 stakingDuration = block.timestamp - userInfo[_user].startTime; + + if (stakingDuration >= BOOST_THRESHOLD_3) return 200; // 2x boost + if (stakingDuration >= BOOST_THRESHOLD_2) return 150; // 1.5x boost + if (stakingDuration >= BOOST_THRESHOLD_1) return 125; // 1.25x boost + return 100; // No boost } /** @@ -172,8 +251,13 @@ contract YieldFarm is ReentrancyGuard, Ownable { // Requirements: // 1. Update rewards before changing rate // 2. Set new reward rate - } + require(_newRate > 0, "Reward rate must be greater than 0"); + + updateReward(address(0)); + rewardRate = _newRate; + } + /** * @notice View function to see pending rewards for a user * @param _user Address of the user diff --git a/challenge-2-yield-farm/package-lock.json b/challenge-2-yield-farm/package-lock.json index 4cbdfb0..d60931b 100644 --- a/challenge-2-yield-farm/package-lock.json +++ b/challenge-2-yield-farm/package-lock.json @@ -9,7 +9,8 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@openzeppelin/contracts": "^5.1.0" + "@openzeppelin/contracts": "^5.1.0", + "dotenv": "^16.5.0" }, "devDependencies": { "@nomicfoundation/hardhat-toolbox": "^5.0.0", @@ -3383,6 +3384,18 @@ "node": ">=8" } }, + "node_modules/dotenv": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.0.tgz", diff --git a/challenge-2-yield-farm/package.json b/challenge-2-yield-farm/package.json index 5366ac8..1d307cb 100644 --- a/challenge-2-yield-farm/package.json +++ b/challenge-2-yield-farm/package.json @@ -17,6 +17,7 @@ "hardhat": "^2.22.17" }, "dependencies": { - "@openzeppelin/contracts": "^5.1.0" + "@openzeppelin/contracts": "^5.1.0", + "dotenv": "^16.5.0" } } diff --git a/challenge-3-frontend/app/page.tsx b/challenge-3-frontend/app/page.tsx index fb01185..1ba8b60 100644 --- a/challenge-3-frontend/app/page.tsx +++ b/challenge-3-frontend/app/page.tsx @@ -26,6 +26,12 @@ export default function Home() {
  • Mint/Redeem LST Bifrost
  • +
  • + Token Vesting +
  • +
  • + Yield Farm +
  • + +
    + + + + +
    + +
    + ) +} \ No newline at end of file diff --git a/challenge-3-frontend/app/yield-farm/page.tsx b/challenge-3-frontend/app/yield-farm/page.tsx new file mode 100644 index 0000000..ff26494 --- /dev/null +++ b/challenge-3-frontend/app/yield-farm/page.tsx @@ -0,0 +1,15 @@ +"use client"; +import YieldFarm from "@/components/yield-farm"; +import SigpassKit from "@/components/sigpasskit"; +import Navbar from "@/components/navbar"; + +export default function YieldFarmPage() { + return ( +
    + + +

    Yield Farming

    + +
    + ); +} diff --git a/challenge-3-frontend/components/token-vesting.tsx b/challenge-3-frontend/components/token-vesting.tsx new file mode 100644 index 0000000..e69de29 diff --git a/challenge-3-frontend/components/yield-farm.tsx b/challenge-3-frontend/components/yield-farm.tsx new file mode 100644 index 0000000..18e8619 --- /dev/null +++ b/challenge-3-frontend/components/yield-farm.tsx @@ -0,0 +1,529 @@ +"use client"; + +// React +import { useState, useEffect } from "react"; + +// Wagmi +import { + type BaseError, + useWaitForTransactionReceipt, + useConfig, + useWriteContract, + useReadContracts, + useAccount +} from "wagmi"; + +// viem +import { parseUnits, formatUnits } from "viem"; + +// Lucide (for icons) +import { + Ban, + ExternalLink, + ChevronDown, + X, + Hash, + LoaderCircle, + CircleCheck, + WalletMinimal, + HandCoins, +} from "lucide-react"; + +// zod (for form validation) +import { z } from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; + +// react-hook-form (for form handling) +import { useForm } from "react-hook-form"; + +// UI components +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Input } from "@/components/ui/input"; +import { useMediaQuery } from "@/hooks/use-media-query"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogFooter, + DialogTitle, + DialogTrigger, + DialogClose, +} from "@/components/ui/dialog"; +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerDescription, + DrawerFooter, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from "@/components/ui/drawer"; +import { Skeleton } from "@/components/ui/skeleton"; + +// utils imports +import { truncateHash } from "@/lib/utils"; + +// components imports +import CopyButton from "@/components/copy-button"; +import { getSigpassWallet } from "@/lib/sigpass"; + +// jotai for state management +import { useAtomValue } from "jotai"; +import { addressAtom } from "@/components/sigpasskit"; + +// config +import { localConfig } from "@/app/providers"; + +// abi for the Moonbeam SLPX contract and ERC20 token +import { mockErc20Abi, yieldFarmingAbi } from "@/lib/abi"; +import { + lpTokenAddress, + rewardTokenAddress, + yieldFarmingAddress +} from "@/lib/constants"; + +export default function YieldFarm() { + // useConfig hook to get config + const config = useConfig(); + + // useAccount hook to get account + const account = useAccount(); + + // useMediaQuery hook to check if the screen is desktop + const isDesktop = useMediaQuery("(min-width: 768px)"); + // useState hook to open/close dialog/drawer + const [open, setOpen] = useState(false); + + // get the address from session storage + const address = useAtomValue(addressAtom); + + // useWriteContract hook to write contract + const { + data: hash, + error, + isPending, + writeContractAsync, + } = useWriteContract({ + config: address ? localConfig : config, + }); + + // form schema for sending transaction + const formSchema = z.object({ + // amount is a required field + amount: z + .string() + .refine((val) => !isNaN(parseFloat(val)) && parseFloat(val) > 0, { + message: "Amount must be a positive number", + }) + .refine((val) => /^\d*\.?\d{0,18}$/.test(val), { + message: "Amount cannot have more than 18 decimal places", + }) + .superRefine((val, ctx) => { + if (!maxBalance || !decimals) return; + + const inputAmount = parseUnits(val, decimals as number); + + if (inputAmount > (maxBalance as bigint)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Amount exceeds available balance", + }); + } + }), + }); + + // 1. Define your form. + const form = useForm>({ + // resolver is zodResolver + resolver: zodResolver(formSchema), + // default values for address and amount + defaultValues: { + amount: "", + }, + }); + + + + // useReadContracts hook to read contract + const { data, refetch: refetchBalance } = useReadContracts({ + contracts: [ + { + address: lpTokenAddress, + abi: mockErc20Abi, + functionName: "balanceOf", + args: [address ? address : account.address], + }, + { + address: lpTokenAddress, + abi: mockErc20Abi, + functionName: "symbol", + }, + { + address: lpTokenAddress, + abi: mockErc20Abi, + functionName: "decimals", + }, + { + // get the allowance of the selected token + address: lpTokenAddress, + abi: mockErc20Abi, + functionName: "allowance", + args: [address ? address : account.address, yieldFarmingAddress], + }, + { + address: yieldFarmingAddress, + abi: yieldFarmingAbi, + functionName: "pendingRewards", + args: [address ? address : account.address], + }, + ], + config: address ? localConfig : config, + }); + + const isOnchainLoading = data === undefined; + + + // extract the data from the read contracts hook + const maxBalance = data?.[0]?.result as bigint | undefined; // balance of the selected token + const symbol = data?.[1]?.result as string | undefined; // symbol of the selected token + const decimals = data?.[2]?.result as number | undefined; // decimals of the selected token + const mintAllowance = data?.[3]?.result as bigint | undefined; // allowance of the selected token + const pendingRewards = data?.[4]?.result as bigint | undefined; + // extract the amount value from the form + const amount = form.watch("amount"); + + // check if the amount is greater than the mint allowance +// const needsApprove = mintAllowance !== undefined && +// amount ? +// mintAllowance < parseUnits(amount, decimals || 18) : +// false; + + const needsApprove = typeof mintAllowance === "bigint" &&typeof decimals === "number" && amount && mintAllowance < parseUnits(amount, decimals); + +console.log("needsApprove", needsApprove); + console.log("mintAllowance", mintAllowance); + console.log("maxBalance", maxBalance); + + + // 2. Define a submit handler. + async function onSubmit(values: z.infer) { + console.log("Submitting form with values:", values); + try { + if (needsApprove) { + console.log("Approving..."); + await writeContractAsync({ + address: lpTokenAddress, + abi: mockErc20Abi, + functionName: "approve", + args: [yieldFarmingAddress, parseUnits(values.amount, decimals as number)], + }); + } else { + console.log("Staking..."); + await writeContractAsync({ + address: yieldFarmingAddress, + abi: yieldFarmingAbi, + functionName: "stake", + args: [parseUnits(values.amount, decimals as number)], + }); + } + } catch (error) { + console.error("Error during transaction:", error); + } + } + + // Watch for transaction hash and open dialog/drawer when received + useEffect(() => { + if (hash) { + setOpen(true); + } + }, [hash]); + + // useWaitForTransactionReceipt hook to wait for transaction receipt + const { isLoading: isConfirming, isSuccess: isConfirmed } = + useWaitForTransactionReceipt({ + hash, + config: address ? localConfig : config, + }); + + // when isConfirmed, refetch the balance of the address + useEffect(() => { + if (isConfirmed) { + refetchBalance(); + } + }, [isConfirmed, refetchBalance]); + + // Find the chain ID from the connected account + const chainId = account.chainId; + + // Get the block explorer URL for the current chain using the config object + function getBlockExplorerUrl(chainId: number | undefined): string | undefined { + const chain = config.chains?.find(chain => chain.id === chainId); + return chain?.blockExplorers?.default?.url || config.chains?.[0]?.blockExplorers?.default?.url; + } + + return ( + + + Stake + Withdraw + + +
    +
    + +
    +
    + + ( + +
    + Amount to stake +
    + {" "} + {maxBalance ? ( + formatUnits(maxBalance as bigint, decimals as number) + ) : ( + + )}{" "} + LP Token +
    +
    + + {isDesktop ? ( + + ) : ( + + )} + + + The amount of LPToken to stake + + +
    + )} + /> + + + + { + // Desktop would be using dialog + isDesktop ? ( + + + + + + + Transaction status + + + Follow the transaction status below. + +
    + {hash ? ( +
    + + Transaction Hash + + {truncateHash(hash)} + + + +
    + ) : ( +
    + + No transaction hash +
    + )} + {!isPending && !isConfirmed && !isConfirming && ( +
    + No transaction submitted +
    + )} + {isConfirming && ( +
    + {" "} + Waiting for confirmation... +
    + )} + {isConfirmed && ( +
    + Transaction + confirmed! +
    + )} + {error && ( +
    + Error:{" "} + {(error as BaseError).shortMessage || error.message} +
    + )} +
    + + + + + + + + ) : ( + // Mobile would be using drawer + + + + + + + Transaction status + + Follow the transaction status below. + + +
    + {hash ? ( +
    + + Transaction Hash + + {truncateHash(hash)} + + + +
    + ) : ( +
    + + No transaction hash +
    + )} + {!isPending && !isConfirmed && !isConfirming && ( +
    + No transaction submitted +
    + )} + {isConfirming && ( +
    + {" "} + Waiting for confirmation... +
    + )} + {isConfirmed && ( +
    + Transaction + confirmed! +
    + )} + {error && ( +
    + Error:{" "} + {(error as BaseError).shortMessage || error.message} +
    + )} +
    + + + + + +
    +
    + ) + } +
    + + Placeholder + + ); +} + diff --git a/challenge-3-frontend/lib/abi.ts b/challenge-3-frontend/lib/abi.ts index 78ac757..4493b95 100644 --- a/challenge-3-frontend/lib/abi.ts +++ b/challenge-3-frontend/lib/abi.ts @@ -2416,4 +2416,1210 @@ export const moonbeamSlpxAbi = [ "stateMutability": "payable", "type": "constructor" } -]; \ No newline at end of file +]; + +export const mockErc20Abi = [ + { + "inputs": [ + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "symbol", + "type": "string" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "allowance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "needed", + "type": "uint256" + } + ], + "name": "ERC20InsufficientAllowance", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "needed", + "type": "uint256" + } + ], + "name": "ERC20InsufficientBalance", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "approver", + "type": "address" + } + ], + "name": "ERC20InvalidApprover", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "receiver", + "type": "address" + } + ], + "name": "ERC20InvalidReceiver", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "ERC20InvalidSender", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "ERC20InvalidSpender", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "mint", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } +] + +export const tokenVestingAbi = [ + { + "inputs": [ + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "EnforcedPause", + "type": "error" + }, + { + "inputs": [], + "name": "ExpectedPause", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "OwnableInvalidOwner", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "OwnableUnauthorizedAccount", + "type": "error" + }, + { + "inputs": [], + "name": "ReentrancyGuardReentrantCall", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beneficiary", + "type": "address" + } + ], + "name": "BeneficiaryRemovedFromWhitelist", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beneficiary", + "type": "address" + } + ], + "name": "BeneficiaryWhitelisted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beneficiary", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "TokensClaimed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beneficiary", + "type": "address" + } + ], + "name": "VestingRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beneficiary", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "VestingScheduleCreated", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "beneficiary", + "type": "address" + } + ], + "name": "addToWhitelist", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "beneficiary", + "type": "address" + } + ], + "name": "calculateVestedAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "claimVestedTokens", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "beneficiary", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "cliffDuration", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "vestingDuration", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "startTime", + "type": "uint256" + } + ], + "name": "createVestingSchedule", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "paused", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "beneficiary", + "type": "address" + } + ], + "name": "removeFromWhitelist", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "beneficiary", + "type": "address" + } + ], + "name": "revokeVesting", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "token", + "outputs": [ + { + "internalType": "contract IERC20", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "vestingSchedules", + "outputs": [ + { + "internalType": "uint256", + "name": "totalAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "startTime", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "cliffDuration", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "vestingDuration", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountClaimed", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "revoked", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "whitelist", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + } +]; + + +export const yieldFarmingAbi = [ + { + "inputs": [ + { + "internalType": "address", + "name": "_lpToken", + "type": "address" + }, + { + "internalType": "address", + "name": "_rewardToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_rewardRate", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "OwnableInvalidOwner", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "OwnableUnauthorizedAccount", + "type": "error" + }, + { + "inputs": [], + "name": "ReentrancyGuardReentrantCall", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "EmergencyWithdrawn", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "RewardsClaimed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Staked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Withdrawn", + "type": "event" + }, + { + "inputs": [], + "name": "BOOST_THRESHOLD_1", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "BOOST_THRESHOLD_2", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "BOOST_THRESHOLD_3", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_user", + "type": "address" + } + ], + "name": "calculateBoostMultiplier", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "claimRewards", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_user", + "type": "address" + } + ], + "name": "earned", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "emergencyWithdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "lastUpdateTime", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "lpToken", + "outputs": [ + { + "internalType": "contract IERC20", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_user", + "type": "address" + } + ], + "name": "pendingRewards", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "rewardPerToken", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "rewardPerTokenStored", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "rewardRate", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "rewardToken", + "outputs": [ + { + "internalType": "contract IERC20", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "stake", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "totalStaked", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_newRate", + "type": "uint256" + } + ], + "name": "updateRewardRate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "userInfo", + "outputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "startTime", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "rewardDebt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "pendingRewards", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "withdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/challenge-3-frontend/lib/constants.ts b/challenge-3-frontend/lib/constants.ts new file mode 100644 index 0000000..4662ab6 --- /dev/null +++ b/challenge-3-frontend/lib/constants.ts @@ -0,0 +1,5 @@ +export const tokenVestingAddress = "0x1f4d7a2b8c5e3c9d6f3b5e4a2f8c5e3c9d6f3b5e4a"; +export const yieldFarmingAddress = "0xF3d9dB094AFB0afd6f3B7555120a66adDF8fbB3E"; +export const tokenAddress = "0x3f4d7a2b8c5e3c9d6f3b5e4a2f8c5e3c9d6f3b5e4a"; +export const lpTokenAddress = "0xE894649758b4E9A1cfb41Cccd2C988aF140e86f3"; +export const rewardTokenAddress = "0xd2c6CfA126ea713859604B400CC2ebD1Fc33F24D"; \ No newline at end of file