From d037090b8fd58c9a4873869bf05a87601afccbac Mon Sep 17 00:00:00 2001 From: jerrymusaga Date: Wed, 30 Apr 2025 15:26:39 +0100 Subject: [PATCH 1/8] Jerry Musaga | Register for OpenGuild x Encode Club Challenges --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ee62858..cd3f56c 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ git clone https://github.com/openguild-labs/open-encode-challenges.git Go to **Participant Registration** section and register to be the workshop participants. Add the below to the list, replace any placeholder with your personal information. ``` -| 🦄 | Name | Github username | Your current occupation | +| 🦄 | Jerry Musaga | jerrymusaga | Web3 developer | ``` - Step 5: `Commit` your code and push to the forked Github repository From 6062f452f602f58a051c108308a989a37ebe4326 Mon Sep 17 00:00:00 2001 From: jerrymusaga Date: Wed, 30 Apr 2025 23:10:47 +0100 Subject: [PATCH 2/8] Implement comprehensive token vesting contract with multi-token support --- .../contracts/TokenVesting.sol | 233 ++++++++++++++++-- challenge-1-vesting/contracts/token.sol | 7 +- 2 files changed, 212 insertions(+), 28 deletions(-) diff --git a/challenge-1-vesting/contracts/TokenVesting.sol b/challenge-1-vesting/contracts/TokenVesting.sol index 43d4c3a..1cb0a6c 100644 --- a/challenge-1-vesting/contracts/TokenVesting.sol +++ b/challenge-1-vesting/contracts/TokenVesting.sol @@ -28,41 +28,72 @@ import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/utils/Pausable.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; -contract TokenVesting is Ownable(msg.sender), Pausable, ReentrancyGuard { + +contract TokenVesting is Ownable, Pausable, ReentrancyGuard { struct VestingSchedule { - // TODO: Define the vesting schedule struct + address tokenAddress; + uint256 totalAmount; + uint256 startTime; + uint256 cliffDuration; + uint256 vestingDuration; + uint256 amountClaimed; + bool revoked; } - // Token being vested - // TODO: Add state variables - + + error InvalidAddress(); + error BeneficiaryNotWhitelisted(address beneficiary); + error InvalidAmount(); + error InvalidDuration(); + error InvalidStartTime(); + error BeneficiaryAlreadyHasSchedule(address beneficiary, address token); + error InsufficientAllowance(uint256 required, uint256 actual); + error TokenTransferFailed(); + error NoVestingScheduleFound(address beneficiary, address token); + error VestingScheduleRevoked(); + error NoVestedTokensAvailable(); - // Mapping from beneficiary to vesting schedule - // TODO: Add state variables + // Mapping from beneficiary to token address to vesting schedule + // This allows one beneficiary to have multiple vesting schedules for different tokens + mapping(address => mapping(address => VestingSchedule)) public vestingSchedules; + + // Track all tokens a beneficiary has schedules for + mapping(address => address[]) public beneficiaryTokens; + + // Track if a beneficiary has a schedule for a specific token + mapping(address => mapping(address => bool)) private hasScheduleForToken; // Whitelist of beneficiaries - // TODO: Add state variables + mapping(address => bool) public whitelist; // Events - event VestingScheduleCreated(address indexed beneficiary, uint256 amount); - event TokensClaimed(address indexed beneficiary, uint256 amount); - event VestingRevoked(address indexed beneficiary); + event VestingScheduleCreated( + address indexed beneficiary, + address indexed tokenAddress, + uint256 amount, + uint256 startTime, + uint256 cliffDuration, + uint256 vestingDuration + ); + event TokensClaimed(address indexed beneficiary, address indexed tokenAddress, uint256 amount); + event VestingRevoked(address indexed beneficiary, address indexed tokenAddress, uint256 unvestedAmount); event BeneficiaryWhitelisted(address indexed beneficiary); event BeneficiaryRemovedFromWhitelist(address indexed beneficiary); - constructor(address tokenAddress) { - // TODO: Initialize the contract - - } + constructor() Ownable(msg.sender) {} // Modifier to check if beneficiary is whitelisted modifier onlyWhitelisted(address beneficiary) { - require(whitelist[beneficiary], "Beneficiary not whitelisted"); + if (!whitelist[beneficiary]) { + revert BeneficiaryNotWhitelisted(beneficiary); + } _; } function addToWhitelist(address beneficiary) external onlyOwner { - require(beneficiary != address(0), "Invalid address"); + if (beneficiary == address(0)) { + revert InvalidAddress(); + } whitelist[beneficiary] = true; emit BeneficiaryWhitelisted(beneficiary); } @@ -74,27 +105,180 @@ contract TokenVesting is Ownable(msg.sender), Pausable, ReentrancyGuard { function createVestingSchedule( address beneficiary, + address tokenAddress, uint256 amount, uint256 cliffDuration, uint256 vestingDuration, uint256 startTime ) external onlyOwner onlyWhitelisted(beneficiary) whenNotPaused { - // TODO: Implement vesting schedule creation + if (beneficiary == address(0)) { + revert InvalidAddress(); + } + if (tokenAddress == address(0)) { + revert InvalidAddress(); + } + if (amount == 0) { + revert InvalidAmount(); + } + if (vestingDuration == 0) { + revert InvalidDuration(); + } + if (vestingDuration < cliffDuration) { + revert InvalidDuration(); + } + if (startTime < block.timestamp) { + revert InvalidStartTime(); + } + if (hasScheduleForToken[beneficiary][tokenAddress]) { + revert BeneficiaryAlreadyHasSchedule(beneficiary, tokenAddress); + } + + IERC20 token = IERC20(tokenAddress); + uint256 allowance = token.allowance(msg.sender, address(this)); + if (allowance < amount) { + revert InsufficientAllowance(amount, allowance); + } + + bool success = token.transferFrom(msg.sender, address(this), amount); + if (!success) { + revert TokenTransferFailed(); + } + + vestingSchedules[beneficiary][tokenAddress] = VestingSchedule({ + tokenAddress: tokenAddress, + totalAmount: amount, + startTime: startTime, + cliffDuration: cliffDuration, + vestingDuration: vestingDuration, + amountClaimed: 0, + revoked: false + }); + + if (!hasScheduleForToken[beneficiary][tokenAddress]) { + beneficiaryTokens[beneficiary].push(tokenAddress); + hasScheduleForToken[beneficiary][tokenAddress] = true; + } + + emit VestingScheduleCreated( + beneficiary, + tokenAddress, + amount, + startTime, + cliffDuration, + vestingDuration + ); } function calculateVestedAmount( - address beneficiary + address beneficiary, + address tokenAddress ) public view returns (uint256) { - // TODO: Implement vested amount calculation + VestingSchedule storage schedule = vestingSchedules[beneficiary][tokenAddress]; + if (schedule.totalAmount == 0) { + return 0; + } + + if (schedule.revoked) { + return 0; + } + + uint256 currentTime = block.timestamp; + if (currentTime < schedule.startTime + schedule.cliffDuration) { + return 0; + } + + if (currentTime >= schedule.startTime + schedule.vestingDuration) { + return schedule.totalAmount - schedule.amountClaimed; + } + + uint256 elapsedTime = currentTime - schedule.startTime; + uint256 vestedAmount = (schedule.totalAmount * elapsedTime) / + schedule.vestingDuration; + return vestedAmount - schedule.amountClaimed; } - function claimVestedTokens() external nonReentrant whenNotPaused { - // TODO: Implement token claiming + + function claimVestedTokens(address tokenAddress) external nonReentrant whenNotPaused { + VestingSchedule storage schedule = vestingSchedules[msg.sender][tokenAddress]; + if (schedule.totalAmount == 0) { + revert NoVestingScheduleFound(msg.sender, tokenAddress); + } + if (schedule.revoked) { + revert VestingScheduleRevoked(); + } + + uint256 vestedAmount = calculateVestedAmount(msg.sender, tokenAddress); + if (vestedAmount == 0) { + revert NoVestedTokensAvailable(); + } + + schedule.amountClaimed += vestedAmount; + + IERC20 token = IERC20(tokenAddress); + bool success = token.transfer(msg.sender, vestedAmount); + if (!success) { + revert TokenTransferFailed(); + } + + emit TokensClaimed(msg.sender, tokenAddress, vestedAmount); } - function revokeVesting(address beneficiary) external onlyOwner { - // TODO: Implement vesting revocation + function revokeVesting( + address beneficiary, + address tokenAddress + ) external onlyOwner nonReentrant { + VestingSchedule storage schedule = vestingSchedules[beneficiary][tokenAddress]; + if (schedule.totalAmount == 0) { + revert NoVestingScheduleFound(beneficiary, tokenAddress); + } + if (schedule.revoked) { + revert VestingScheduleRevoked(); + } + + // Calculate vested and unvested amounts + uint256 vestedAmount = calculateVestedAmount(beneficiary, tokenAddress); + uint256 unvestedAmount = schedule.totalAmount - + schedule.amountClaimed - + vestedAmount; + + schedule.revoked = true; + schedule.totalAmount = schedule.amountClaimed + vestedAmount; + + if (unvestedAmount > 0) { + IERC20 token = IERC20(tokenAddress); + bool success = token.transfer(owner(), unvestedAmount); + if (!success) { + revert TokenTransferFailed(); + } + } + emit VestingRevoked(beneficiary, tokenAddress, unvestedAmount); + } + + function getBeneficiaryTokens(address beneficiary) external view returns (address[] memory) { + return beneficiaryTokens[beneficiary]; + } + + function getVestingSchedule( + address beneficiary, + address tokenAddress + ) external view returns ( + uint256 totalAmount, + uint256 startTime, + uint256 cliffDuration, + uint256 vestingDuration, + uint256 amountClaimed, + bool revoked + ) { + VestingSchedule storage schedule = vestingSchedules[beneficiary][tokenAddress]; + return ( + schedule.totalAmount, + schedule.startTime, + schedule.cliffDuration, + schedule.vestingDuration, + schedule.amountClaimed, + schedule.revoked + ); } function pause() external onlyOwner { @@ -105,7 +289,6 @@ contract TokenVesting is Ownable(msg.sender), Pausable, ReentrancyGuard { _unpause(); } } - /* Solution template (key points to implement): diff --git a/challenge-1-vesting/contracts/token.sol b/challenge-1-vesting/contracts/token.sol index 5f952a1..d729265 100644 --- a/challenge-1-vesting/contracts/token.sol +++ b/challenge-1-vesting/contracts/token.sol @@ -4,10 +4,11 @@ pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; -contract MockERC20 is ERC20, Ownable(msg.sender) { - constructor(string memory name, string memory symbol) ERC20(name, symbol) {} + +contract MockERC20 is ERC20, Ownable { + constructor(string memory name, string memory symbol) ERC20(name, symbol) Ownable(msg.sender) {} function mint(address to, uint256 amount) external onlyOwner { _mint(to, amount); } -} +} \ No newline at end of file From b3a3718e51c8c29a26d80f23b5d3553a961ebe77 Mon Sep 17 00:00:00 2001 From: jerrymusaga Date: Wed, 30 Apr 2025 23:16:31 +0100 Subject: [PATCH 3/8] Updated test suite to match new multi-token functionality --- challenge-1-vesting/test/vesting.ts | 92 ++++++++++++++++++++--------- 1 file changed, 64 insertions(+), 28 deletions(-) diff --git a/challenge-1-vesting/test/vesting.ts b/challenge-1-vesting/test/vesting.ts index 803c7be..fa6e2fc 100644 --- a/challenge-1-vesting/test/vesting.ts +++ b/challenge-1-vesting/test/vesting.ts @@ -24,14 +24,14 @@ describe("TokenVesting", function () { await token.waitForDeployment(); // Deploy Vesting Contract - const TokenVesting = await ethers.getContractFactory("TokenVesting"); - vesting = await TokenVesting.deploy(await token.getAddress()); + TokenVesting = await ethers.getContractFactory("TokenVesting"); + vesting = await TokenVesting.deploy(); await vesting.waitForDeployment(); - // Mint tokens to owner + // Mint tokens to owner for testing await token.mint(owner.address, ethers.parseEther("10000")); - // Approve vesting contract + // Approve vesting contract to spend tokens await token.approve(await vesting.getAddress(), ethers.parseEther("10000")); startTime = (await time.latest()) + 60; // Start 1 minute from now @@ -39,7 +39,7 @@ describe("TokenVesting", function () { describe("Deployment", function () { it("Should set the right token", async function () { - expect(await vesting.token()).to.equal(await token.getAddress()); + expect(await token.name()).to.equal("Mock Token"); }); it("Should set the right owner", async function () { @@ -68,13 +68,17 @@ describe("TokenVesting", function () { it("Should create vesting schedule", async function () { await vesting.createVestingSchedule( beneficiary.address, + await token.getAddress(), amount, cliffDuration, vestingDuration, startTime ); - const schedule = await vesting.vestingSchedules(beneficiary.address); + const schedule = await vesting.getVestingSchedule( + beneficiary.address, + await token.getAddress() + ); expect(schedule.totalAmount).to.equal(amount); }); @@ -82,12 +86,15 @@ describe("TokenVesting", function () { await expect( vesting.createVestingSchedule( addr2.address, + await token.getAddress(), amount, cliffDuration, vestingDuration, startTime ) - ).to.be.revertedWith("Beneficiary not whitelisted"); + ) + .to.be.revertedWithCustomError(vesting, "BeneficiaryNotWhitelisted") + .withArgs(addr2.address); }); }); @@ -96,6 +103,7 @@ describe("TokenVesting", function () { await vesting.addToWhitelist(beneficiary.address); await vesting.createVestingSchedule( beneficiary.address, + await token.getAddress(), amount, cliffDuration, vestingDuration, @@ -107,19 +115,23 @@ describe("TokenVesting", function () { // Ensure we're past the start time but before cliff await time.increase(60); // Move past start time await expect( - vesting.connect(beneficiary).claimVestedTokens() - ).to.be.revertedWith("No tokens to claim"); + vesting.connect(beneficiary).claimVestedTokens(await token.getAddress()) + ).to.be.revertedWithCustomError(vesting, "NoVestedTokensAvailable"); }); it("Should allow claiming after cliff", async function () { await time.increaseTo(startTime + cliffDuration + vestingDuration / 4); - await vesting.connect(beneficiary).claimVestedTokens(); + await vesting + .connect(beneficiary) + .claimVestedTokens(await token.getAddress()); expect(await token.balanceOf(beneficiary.address)).to.be.above(0); }); it("Should vest full amount after vesting duration", async function () { await time.increaseTo(startTime + vestingDuration + 1); - await vesting.connect(beneficiary).claimVestedTokens(); + await vesting + .connect(beneficiary) + .claimVestedTokens(await token.getAddress()); expect(await token.balanceOf(beneficiary.address)).to.equal(amount); }); }); @@ -129,6 +141,7 @@ describe("TokenVesting", function () { await vesting.addToWhitelist(beneficiary.address); await vesting.createVestingSchedule( beneficiary.address, + await token.getAddress(), amount, cliffDuration, vestingDuration, @@ -137,24 +150,40 @@ describe("TokenVesting", function () { }); it("Should allow owner to revoke vesting", async function () { - await vesting.revokeVesting(beneficiary.address); - const schedule = await vesting.vestingSchedules(beneficiary.address); + await vesting.revokeVesting( + beneficiary.address, + await token.getAddress() + ); + const schedule = await vesting.getVestingSchedule( + beneficiary.address, + await token.getAddress() + ); expect(schedule.revoked).to.be.true; }); it("Should not allow non-owner to revoke vesting", async function () { await expect( - vesting.connect(beneficiary).revokeVesting(beneficiary.address) + vesting + .connect(beneficiary) + .revokeVesting(beneficiary.address, await token.getAddress()) ).to.be.revertedWithCustomError(vesting, "OwnableUnauthorizedAccount"); }); it("Should return unvested tokens to owner when revoking", async function () { const initialOwnerBalance = await token.balanceOf(owner.address); await time.increaseTo(startTime + vestingDuration / 2); // 50% vested - await vesting.revokeVesting(beneficiary.address); + await vesting.revokeVesting( + beneficiary.address, + await token.getAddress() + ); const finalOwnerBalance = await token.balanceOf(owner.address); - expect(finalOwnerBalance - initialOwnerBalance).to.be.closeTo( - amount / BigInt(2), // Approximately 50% of tokens should return to owner + + // Calculate difference + const diff = finalOwnerBalance - initialOwnerBalance; + + // Should be approximately 50% of tokens returned to owner + expect(diff).to.be.closeTo( + amount / 2n, // Approximately 50% of tokens should return to owner ethers.parseEther("1") // Allow for small rounding differences ); }); @@ -163,28 +192,35 @@ describe("TokenVesting", function () { describe("Pausing", function () { beforeEach(async function () { await vesting.addToWhitelist(beneficiary.address); - await vesting.createVestingSchedule( - beneficiary.address, - amount, - cliffDuration, - vestingDuration, - startTime - ); }); it("Should not allow operations when paused", async function () { await vesting.pause(); await expect( - vesting.connect(beneficiary).claimVestedTokens() + vesting.createVestingSchedule( + beneficiary.address, + await token.getAddress(), + amount, + cliffDuration, + vestingDuration, + startTime + ) ).to.be.revertedWithCustomError(vesting, "EnforcedPause"); }); it("Should allow operations after unpause", async function () { await vesting.pause(); await vesting.unpause(); - await time.increaseTo(startTime + vestingDuration); - await expect(vesting.connect(beneficiary).claimVestedTokens()).to.not.be - .reverted; + await expect( + vesting.createVestingSchedule( + beneficiary.address, + await token.getAddress(), + amount, + cliffDuration, + vestingDuration, + startTime + ) + ).to.not.be.reverted; }); }); }); From b872960c88a0a34ad387c5c03d01a1f9afda3860 Mon Sep 17 00:00:00 2001 From: jerrymusaga Date: Thu, 1 May 2025 10:27:09 +0100 Subject: [PATCH 4/8] optimized code by removing openzepellin imports and implement simplified version of it and packed storage --- challenge-1-vesting/README.md | 7 +- .../contracts/TokenVesting.sol | 239 ++++++++---------- .../ignition/modules/token-vesting.ts | 10 +- 3 files changed, 108 insertions(+), 148 deletions(-) diff --git a/challenge-1-vesting/README.md b/challenge-1-vesting/README.md index 9cc7a2c..3cd0a7a 100644 --- a/challenge-1-vesting/README.md +++ b/challenge-1-vesting/README.md @@ -8,9 +8,10 @@ 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 | +| ----- | ----- | --------------------------------------------- | ------------ | +| 🎅 | Ippo | [NTP-996](https://github.com/NTP-996) | DevRel | +| 🎅 | Jerry | [jerrymusaga](https://github.com/jerrymusaga) | Software dev | ## 💻 Local development environment setup diff --git a/challenge-1-vesting/contracts/TokenVesting.sol b/challenge-1-vesting/contracts/TokenVesting.sol index 1cb0a6c..8f4817c 100644 --- a/challenge-1-vesting/contracts/TokenVesting.sol +++ b/challenge-1-vesting/contracts/TokenVesting.sol @@ -24,23 +24,21 @@ Here's your starter code: pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "@openzeppelin/contracts/access/Ownable.sol"; -import "@openzeppelin/contracts/utils/Pausable.sol"; -import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; - -contract TokenVesting is Ownable, Pausable, ReentrancyGuard { +contract TokenVesting { + using SafeERC20 for IERC20; + struct VestingSchedule { address tokenAddress; - uint256 totalAmount; - uint256 startTime; - uint256 cliffDuration; - uint256 vestingDuration; - uint256 amountClaimed; + uint96 totalAmount; + uint32 startTime; + uint32 cliffDuration; + uint32 vestingDuration; + uint96 amountClaimed; bool revoked; } - - + error InvalidAddress(); error BeneficiaryNotWhitelisted(address beneficiary); error InvalidAmount(); @@ -48,23 +46,16 @@ contract TokenVesting is Ownable, Pausable, ReentrancyGuard { error InvalidStartTime(); error BeneficiaryAlreadyHasSchedule(address beneficiary, address token); error InsufficientAllowance(uint256 required, uint256 actual); - error TokenTransferFailed(); error NoVestingScheduleFound(address beneficiary, address token); error VestingScheduleRevoked(); error NoVestedTokensAvailable(); - - // Mapping from beneficiary to token address to vesting schedule - // This allows one beneficiary to have multiple vesting schedules for different tokens - mapping(address => mapping(address => VestingSchedule)) public vestingSchedules; - - // Track all tokens a beneficiary has schedules for - mapping(address => address[]) public beneficiaryTokens; + error OwnableUnauthorizedAccount(address account); + error EnforcedPause(); - // Track if a beneficiary has a schedule for a specific token - mapping(address => mapping(address => bool)) private hasScheduleForToken; - - // Whitelist of beneficiaries - mapping(address => bool) public whitelist; + mapping(address => mapping(address => VestingSchedule)) private _schedules; + mapping(address => bool) private _whitelist; + address private immutable _owner; + bool private _paused; // Events event VestingScheduleCreated( @@ -79,72 +70,65 @@ contract TokenVesting is Ownable, Pausable, ReentrancyGuard { event VestingRevoked(address indexed beneficiary, address indexed tokenAddress, uint256 unvestedAmount); event BeneficiaryWhitelisted(address indexed beneficiary); event BeneficiaryRemovedFromWhitelist(address indexed beneficiary); + event Paused(address account); + event Unpaused(address account); + + constructor() { + _owner = msg.sender; + } - constructor() Ownable(msg.sender) {} + modifier onlyOwner() { + if (msg.sender != _owner) revert OwnableUnauthorizedAccount(msg.sender); + _; + } - // Modifier to check if beneficiary is whitelisted - modifier onlyWhitelisted(address beneficiary) { - if (!whitelist[beneficiary]) { - revert BeneficiaryNotWhitelisted(beneficiary); - } + modifier whenNotPaused() { + if (_paused) revert EnforcedPause(); _; } + function owner() public view returns (address) { + return _owner; + } + + function pause() external onlyOwner { + _paused = true; + emit Paused(msg.sender); + } + + function unpause() external onlyOwner { + _paused = false; + emit Unpaused(msg.sender); + } + function addToWhitelist(address beneficiary) external onlyOwner { - if (beneficiary == address(0)) { - revert InvalidAddress(); - } - whitelist[beneficiary] = true; + if (beneficiary == address(0)) revert InvalidAddress(); + _whitelist[beneficiary] = true; emit BeneficiaryWhitelisted(beneficiary); } function removeFromWhitelist(address beneficiary) external onlyOwner { - whitelist[beneficiary] = false; + _whitelist[beneficiary] = false; emit BeneficiaryRemovedFromWhitelist(beneficiary); } function createVestingSchedule( address beneficiary, address tokenAddress, - uint256 amount, - uint256 cliffDuration, - uint256 vestingDuration, - uint256 startTime - ) external onlyOwner onlyWhitelisted(beneficiary) whenNotPaused { - if (beneficiary == address(0)) { - revert InvalidAddress(); - } - if (tokenAddress == address(0)) { - revert InvalidAddress(); - } - if (amount == 0) { - revert InvalidAmount(); - } - if (vestingDuration == 0) { - revert InvalidDuration(); - } - if (vestingDuration < cliffDuration) { - revert InvalidDuration(); - } - if (startTime < block.timestamp) { - revert InvalidStartTime(); - } - if (hasScheduleForToken[beneficiary][tokenAddress]) { - revert BeneficiaryAlreadyHasSchedule(beneficiary, tokenAddress); - } - + uint96 amount, + uint32 cliffDuration, + uint32 vestingDuration, + uint32 startTime + ) external onlyOwner whenNotPaused { + _validateVestingParams(beneficiary, tokenAddress, amount, cliffDuration, vestingDuration, startTime); + IERC20 token = IERC20(tokenAddress); uint256 allowance = token.allowance(msg.sender, address(this)); - if (allowance < amount) { - revert InsufficientAllowance(amount, allowance); - } - - bool success = token.transferFrom(msg.sender, address(this), amount); - if (!success) { - revert TokenTransferFailed(); - } + if (allowance < amount) revert InsufficientAllowance(amount, allowance); - vestingSchedules[beneficiary][tokenAddress] = VestingSchedule({ + token.safeTransferFrom(msg.sender, address(this), amount); + + _schedules[beneficiary][tokenAddress] = VestingSchedule({ tokenAddress: tokenAddress, totalAmount: amount, startTime: startTime, @@ -154,11 +138,6 @@ contract TokenVesting is Ownable, Pausable, ReentrancyGuard { revoked: false }); - if (!hasScheduleForToken[beneficiary][tokenAddress]) { - beneficiaryTokens[beneficiary].push(tokenAddress); - hasScheduleForToken[beneficiary][tokenAddress] = true; - } - emit VestingScheduleCreated( beneficiary, tokenAddress, @@ -169,96 +148,82 @@ contract TokenVesting is Ownable, Pausable, ReentrancyGuard { ); } + function _validateVestingParams( + address beneficiary, + address tokenAddress, + uint96 amount, + uint32 cliffDuration, + uint32 vestingDuration, + uint32 startTime + ) private view { + if (beneficiary == address(0) || tokenAddress == address(0)) revert InvalidAddress(); + if (amount == 0) revert InvalidAmount(); + if (vestingDuration == 0 || vestingDuration < cliffDuration) revert InvalidDuration(); + if (startTime < uint32(block.timestamp)) revert InvalidStartTime(); + if (_schedules[beneficiary][tokenAddress].totalAmount != 0) { + revert BeneficiaryAlreadyHasSchedule(beneficiary, tokenAddress); + } + if (!_whitelist[beneficiary]) revert BeneficiaryNotWhitelisted(beneficiary); + } + function calculateVestedAmount( address beneficiary, address tokenAddress ) public view returns (uint256) { - VestingSchedule storage schedule = vestingSchedules[beneficiary][tokenAddress]; - if (schedule.totalAmount == 0) { - return 0; - } - - if (schedule.revoked) { + VestingSchedule storage schedule = _schedules[beneficiary][tokenAddress]; + + if (schedule.totalAmount == 0 || schedule.revoked) { return 0; } - uint256 currentTime = block.timestamp; - if (currentTime < schedule.startTime + schedule.cliffDuration) { + if (block.timestamp < schedule.startTime + schedule.cliffDuration) { return 0; } - if (currentTime >= schedule.startTime + schedule.vestingDuration) { + if (block.timestamp >= schedule.startTime + schedule.vestingDuration) { return schedule.totalAmount - schedule.amountClaimed; } - uint256 elapsedTime = currentTime - schedule.startTime; - uint256 vestedAmount = (schedule.totalAmount * elapsedTime) / - schedule.vestingDuration; + uint256 elapsedTime = block.timestamp - schedule.startTime; + uint256 vestedAmount = (uint256(schedule.totalAmount) * elapsedTime) / schedule.vestingDuration; return vestedAmount - schedule.amountClaimed; } - - function claimVestedTokens(address tokenAddress) external nonReentrant whenNotPaused { - VestingSchedule storage schedule = vestingSchedules[msg.sender][tokenAddress]; - if (schedule.totalAmount == 0) { - revert NoVestingScheduleFound(msg.sender, tokenAddress); - } - if (schedule.revoked) { - revert VestingScheduleRevoked(); - } + function claimVestedTokens(address tokenAddress) external whenNotPaused { + VestingSchedule storage schedule = _schedules[msg.sender][tokenAddress]; + + if (schedule.totalAmount == 0) revert NoVestingScheduleFound(msg.sender, tokenAddress); + if (schedule.revoked) revert VestingScheduleRevoked(); uint256 vestedAmount = calculateVestedAmount(msg.sender, tokenAddress); - if (vestedAmount == 0) { - revert NoVestedTokensAvailable(); - } + if (vestedAmount == 0) revert NoVestedTokensAvailable(); - schedule.amountClaimed += vestedAmount; + schedule.amountClaimed += uint96(vestedAmount); - IERC20 token = IERC20(tokenAddress); - bool success = token.transfer(msg.sender, vestedAmount); - if (!success) { - revert TokenTransferFailed(); - } + IERC20(tokenAddress).safeTransfer(msg.sender, vestedAmount); emit TokensClaimed(msg.sender, tokenAddress, vestedAmount); } - function revokeVesting( - address beneficiary, - address tokenAddress - ) external onlyOwner nonReentrant { - VestingSchedule storage schedule = vestingSchedules[beneficiary][tokenAddress]; - if (schedule.totalAmount == 0) { - revert NoVestingScheduleFound(beneficiary, tokenAddress); - } - if (schedule.revoked) { - revert VestingScheduleRevoked(); - } + function revokeVesting(address beneficiary, address tokenAddress) external onlyOwner { + VestingSchedule storage schedule = _schedules[beneficiary][tokenAddress]; + + if (schedule.totalAmount == 0) revert NoVestingScheduleFound(beneficiary, tokenAddress); + if (schedule.revoked) revert VestingScheduleRevoked(); - // Calculate vested and unvested amounts uint256 vestedAmount = calculateVestedAmount(beneficiary, tokenAddress); - uint256 unvestedAmount = schedule.totalAmount - - schedule.amountClaimed - - vestedAmount; + uint256 unvestedAmount = schedule.totalAmount - schedule.amountClaimed - vestedAmount; schedule.revoked = true; - schedule.totalAmount = schedule.amountClaimed + vestedAmount; + schedule.totalAmount = uint96(schedule.amountClaimed + vestedAmount); if (unvestedAmount > 0) { - IERC20 token = IERC20(tokenAddress); - bool success = token.transfer(owner(), unvestedAmount); - if (!success) { - revert TokenTransferFailed(); - } + IERC20(tokenAddress).safeTransfer(_owner, unvestedAmount); } emit VestingRevoked(beneficiary, tokenAddress, unvestedAmount); } - function getBeneficiaryTokens(address beneficiary) external view returns (address[] memory) { - return beneficiaryTokens[beneficiary]; - } - function getVestingSchedule( address beneficiary, address tokenAddress @@ -270,7 +235,7 @@ contract TokenVesting is Ownable, Pausable, ReentrancyGuard { uint256 amountClaimed, bool revoked ) { - VestingSchedule storage schedule = vestingSchedules[beneficiary][tokenAddress]; + VestingSchedule storage schedule = _schedules[beneficiary][tokenAddress]; return ( schedule.totalAmount, schedule.startTime, @@ -281,12 +246,8 @@ contract TokenVesting is Ownable, Pausable, ReentrancyGuard { ); } - function pause() external onlyOwner { - _pause(); - } - - function unpause() external onlyOwner { - _unpause(); + function whitelist(address beneficiary) external view returns (bool) { + return _whitelist[beneficiary]; } } /* diff --git a/challenge-1-vesting/ignition/modules/token-vesting.ts b/challenge-1-vesting/ignition/modules/token-vesting.ts index 199368b..434716a 100644 --- a/challenge-1-vesting/ignition/modules/token-vesting.ts +++ b/challenge-1-vesting/ignition/modules/token-vesting.ts @@ -1,14 +1,12 @@ import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; const VestingModule = buildModule("VestingModule", (m) => { - // Deploy Token first - const token = m.contract("SimpleToken", [], { - id: "simple_token", + const token = m.contract("MockERC20", ["Test Token", "TST"], { + id: "mock_token", }); - // Deploy Vesting Contract with Token address - const vesting = m.contract("SimpleVesting", [token], { - id: "simple_vesting", + const vesting = m.contract("TokenVesting", [], { + id: "token_vesting", }); return { token, vesting }; From 50408371063c508a6bebada21eef26de68b8e830 Mon Sep 17 00:00:00 2001 From: jerrymusaga Date: Thu, 1 May 2025 10:34:47 +0100 Subject: [PATCH 5/8] participant registration --- challenge-1-vesting/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/challenge-1-vesting/README.md b/challenge-1-vesting/README.md index 3cd0a7a..f5bc231 100644 --- a/challenge-1-vesting/README.md +++ b/challenge-1-vesting/README.md @@ -10,7 +10,6 @@ Add your information to the below list to officially participate in the workshop | Emoji | Name | Github Username | Occupations | | ----- | ----- | --------------------------------------------- | ------------ | -| 🎅 | Ippo | [NTP-996](https://github.com/NTP-996) | DevRel | | 🎅 | Jerry | [jerrymusaga](https://github.com/jerrymusaga) | Software dev | ## 💻 Local development environment setup From ecd82311b73c8255d3757ae23270479d18b7fa69 Mon Sep 17 00:00:00 2001 From: jerrymusaga Date: Thu, 1 May 2025 11:12:46 +0100 Subject: [PATCH 6/8] Implementing a yield farming contract with the provided requirements --- challenge-2-yield-farm/contracts/yeild.sol | 139 ++++++++++++++------- challenge-2-yield-farm/package-lock.json | 15 ++- challenge-2-yield-farm/package.json | 3 +- 3 files changed, 109 insertions(+), 48 deletions(-) diff --git a/challenge-2-yield-farm/contracts/yeild.sol b/challenge-2-yield-farm/contracts/yeild.sol index 421496a..3d43888 100644 --- a/challenge-2-yield-farm/contracts/yeild.sol +++ b/challenge-2-yield-farm/contracts/yeild.sol @@ -57,8 +57,6 @@ contract YieldFarm is ReentrancyGuard, Ownable { event RewardsClaimed(address indexed user, uint256 amount); event EmergencyWithdrawn(address indexed user, uint256 amount); - // TODO: Implement the following functions - /** * @notice Initialize the contract with the LP token and reward token addresses * @param _lpToken Address of the LP token @@ -70,7 +68,10 @@ contract YieldFarm is ReentrancyGuard, Ownable { address _rewardToken, uint256 _rewardRate ) Ownable(msg.sender) { - // TODO: Initialize contract state + lpToken = IERC20(_lpToken); + rewardToken = IERC20(_rewardToken); + rewardRate = _rewardRate; + lastUpdateTime = block.timestamp; } function updateReward(address _user) internal { @@ -85,19 +86,27 @@ contract YieldFarm is ReentrancyGuard, Ownable { } function rewardPerToken() public view returns (uint256) { - // TODO: Implement pending rewards calculation - // Requirements: - // 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; + uint256 rewardAccrued = (timeElapsed * rewardRate * 1e18) / totalStaked; + return rewardPerTokenStored + rewardAccrued; } function earned(address _user) public view returns (uint256) { - // TODO: Implement pending rewards calculation - // Requirements: - // 1. Calculate rewards since last update - // 2. Apply boost multiplier - // 3. Return total pending rewards + UserInfo storage user = userInfo[_user]; + if (user.amount == 0) { + return user.pendingRewards; + } + + uint256 currentRewardPerToken = rewardPerToken(); + uint256 newRewards = (user.amount * (currentRewardPerToken - user.rewardDebt / user.amount)) / 1e18; + uint256 totalRewards = user.pendingRewards + newRewards; + + uint256 multiplier = calculateBoostMultiplier(_user); + return (totalRewards * multiplier) / 100; } /** @@ -105,12 +114,21 @@ contract YieldFarm is ReentrancyGuard, Ownable { * @param _amount Amount of LP tokens to stake */ function stake(uint256 _amount) external nonReentrant { - // TODO: Implement staking logic - // Requirements: - // 1. Update rewards - // 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); + + lpToken.transferFrom(msg.sender, address(this), _amount); + + UserInfo storage user = userInfo[msg.sender]; + if (user.amount == 0) { + user.startTime = block.timestamp; + } + + user.amount += _amount; + totalStaked += _amount; + + emit Staked(msg.sender, _amount); } /** @@ -118,49 +136,80 @@ contract YieldFarm is ReentrancyGuard, Ownable { * @param _amount Amount of LP tokens to withdraw */ function withdraw(uint256 _amount) external nonReentrant { - // TODO: Implement withdrawal logic - // Requirements: - // 1. Update rewards - // 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); + + lpToken.transfer(msg.sender, _amount); + + user.amount -= _amount; + totalStaked -= _amount; + + emit Withdrawn(msg.sender, _amount); } /** * @notice Claim pending rewards */ function claimRewards() external nonReentrant { - // TODO: Implement reward claiming logic - // Requirements: - // 1. Calculate pending rewards with boost multiplier - // 2. Transfer rewards to user - // 3. Update user reward debt - // 4. Emit RewardsClaimed event + updateReward(msg.sender); + + UserInfo storage user = userInfo[msg.sender]; + uint256 reward = user.pendingRewards; + + if (reward > 0) { + user.pendingRewards = 0; + rewardToken.transfer(msg.sender, reward); + emit RewardsClaimed(msg.sender, reward); + } } /** * @notice Emergency withdraw without caring about rewards */ function emergencyWithdraw() external nonReentrant { - // TODO: Implement emergency withdrawal - // Requirements: - // 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; + + totalStaked -= amount; + + // Reset user info + user.amount = 0; + user.startTime = 0; + user.rewardDebt = 0; + user.pendingRewards = 0; + + // Return LP tokens to user + lpToken.transfer(msg.sender, amount); + + emit EmergencyWithdrawn(msg.sender, amount); } /** * @notice Calculate boost multiplier based on staking duration * @param _user Address of the user - * @return Boost multiplier (100 = 1x, 150 = 1.5x, etc.) + * @return Boost multiplier (100 = 1x, 125 = 1.25x, 150 = 1.5x, 200 = 2x) */ function calculateBoostMultiplier( address _user ) public view returns (uint256) { - // TODO: Implement boost multiplier calculation - // Requirements: - // 1. Calculate staking duration - // 2. Return appropriate multiplier based on duration thresholds + UserInfo storage user = userInfo[_user]; + if (user.amount == 0 || user.startTime == 0) { + return 100; // Base multiplier (1x) + } + + uint256 stakingDuration = block.timestamp - user.startTime; + + if (stakingDuration >= BOOST_THRESHOLD_3) { + return 200; // 2x multiplier + } else if (stakingDuration >= BOOST_THRESHOLD_2) { + return 150; // 1.5x multiplier + } else if (stakingDuration >= BOOST_THRESHOLD_1) { + return 125; // 1.25x multiplier + } else { + return 100; // Base multiplier (1x) + } } /** @@ -168,10 +217,8 @@ contract YieldFarm is ReentrancyGuard, Ownable { * @param _newRate New reward rate per second */ function updateRewardRate(uint256 _newRate) external onlyOwner { - // TODO: Implement reward rate update logic - // Requirements: - // 1. Update rewards before changing rate - // 2. Set new reward rate + updateReward(address(0)); // Update rewards for all users before changing rate + rewardRate = _newRate; } /** @@ -182,4 +229,4 @@ contract YieldFarm is ReentrancyGuard, Ownable { function pendingRewards(address _user) external view returns (uint256) { return earned(_user); } -} +} \ No newline at end of file diff --git a/challenge-2-yield-farm/package-lock.json b/challenge-2-yield-farm/package-lock.json index 4cbdfb0..8910070 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.4.7" }, "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..51ad4b9 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.4.7" } } From a992cf14742172c1c237268584495dabce228fd4 Mon Sep 17 00:00:00 2001 From: jerrymusaga Date: Wed, 7 May 2025 17:50:07 +0100 Subject: [PATCH 7/8] finalizing challenge 3 for the frontend --- challenge-2-yield-farm/contracts/yeild.sol | 267 +- challenge-3-frontend/.gitignore | 25 +- challenge-3-frontend/app/layout.tsx | 24 +- .../app/mint-redeem-lst-bifrost/page.tsx | 28 +- challenge-3-frontend/app/page.tsx | 307 +- challenge-3-frontend/app/providers.tsx | 63 +- .../app/send-transaction/page.tsx | 27 +- challenge-3-frontend/app/vesting/page.tsx | 19 + .../app/write-contract/page.tsx | 30 +- .../components/create-wallet-dialog.tsx | 154 + .../components/mint-redeem-lst-bifrost.tsx | 633 +- challenge-3-frontend/components/navbar.tsx | 233 +- .../components/send-transaction.tsx | 278 +- .../components/sigpasskit.tsx | 556 +- .../components/transaction-status.tsx | 165 + challenge-3-frontend/components/ui/alert.tsx | 62 + challenge-3-frontend/components/ui/button.tsx | 4 +- challenge-3-frontend/components/ui/card.tsx | 87 + .../components/ui/progress.tsx | 36 + .../components/ui/tooltip.tsx | 122 + .../components/vesting1/CreateVestingForm.tsx | 481 ++ .../components/vesting1/Vesting.tsx | 72 + .../vesting1/VestingScheduleView.tsx | 492 ++ .../vesting1/WhiteListManagement.tsx | 473 ++ .../components/write-contract.tsx | 426 +- challenge-3-frontend/lib/abi.ts | 6100 ++++++++++------- challenge-3-frontend/lib/addresses.ts | 2 + challenge-3-frontend/lib/atoms.ts | 7 + challenge-3-frontend/lib/vestingABI.ts | 482 ++ challenge-3-frontend/package-lock.json | 703 ++ challenge-3-frontend/package.json | 1 + .../webpack/client-development/0.pack.gz | Bin 0 -> 15352 bytes .../webpack/client-development/index.pack.gz | Bin 0 -> 51081 bytes 33 files changed, 8500 insertions(+), 3859 deletions(-) create mode 100644 challenge-3-frontend/app/vesting/page.tsx create mode 100644 challenge-3-frontend/components/create-wallet-dialog.tsx create mode 100644 challenge-3-frontend/components/transaction-status.tsx create mode 100644 challenge-3-frontend/components/ui/alert.tsx create mode 100644 challenge-3-frontend/components/ui/card.tsx create mode 100644 challenge-3-frontend/components/ui/progress.tsx create mode 100644 challenge-3-frontend/components/ui/tooltip.tsx create mode 100644 challenge-3-frontend/components/vesting1/CreateVestingForm.tsx create mode 100644 challenge-3-frontend/components/vesting1/Vesting.tsx create mode 100644 challenge-3-frontend/components/vesting1/VestingScheduleView.tsx create mode 100644 challenge-3-frontend/components/vesting1/WhiteListManagement.tsx create mode 100644 challenge-3-frontend/lib/addresses.ts create mode 100644 challenge-3-frontend/lib/atoms.ts create mode 100644 challenge-3-frontend/lib/vestingABI.ts create mode 100644 challenge-34-frontend/.next/cache/webpack/client-development/0.pack.gz create mode 100644 challenge-34-frontend/.next/cache/webpack/client-development/index.pack.gz diff --git a/challenge-2-yield-farm/contracts/yeild.sol b/challenge-2-yield-farm/contracts/yeild.sol index 3d43888..ea86933 100644 --- a/challenge-2-yield-farm/contracts/yeild.sol +++ b/challenge-2-yield-farm/contracts/yeild.sol @@ -1,232 +1,107 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.20; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; -/** - * @title YieldFarm - * @notice Challenge: Implement a yield farming contract with the following requirements: - * - * 1. Users can stake LP tokens and earn reward tokens - * 2. Rewards are distributed based on time and amount staked - * 3. Implement reward boosting mechanism for long-term stakers - * 4. Add emergency withdrawal functionality - * 5. Implement reward rate adjustment mechanism - */ - -contract YieldFarm is ReentrancyGuard, Ownable { - // LP token that users can stake - IERC20 public lpToken; - - // Token given as reward - IERC20 public rewardToken; - - // Reward rate per second - uint256 public rewardRate; - - // Last update time +contract YieldFarm is Ownable { + using SafeERC20 for IERC20; + + IERC20 public immutable lpToken; + IERC20 public immutable rewardToken; + uint96 public rewardRate; uint256 public lastUpdateTime; - - // Reward per token stored uint256 public rewardPerTokenStored; - - // Total staked amount uint256 public totalStaked; - - // User struct to track staking info + struct UserInfo { - uint256 amount; // Amount of LP tokens staked - uint256 startTime; // Time when user started staking - uint256 rewardDebt; // Reward debt - uint256 pendingRewards; // Unclaimed rewards + uint96 amount; + uint32 startTime; + uint96 rewardDebt; + uint96 pendingRewards; } - - // Mapping of user address to their info + mapping(address => UserInfo) public userInfo; - - // Boost multiplier thresholds (in seconds) - uint256 public constant BOOST_THRESHOLD_1 = 7 days; - uint256 public constant BOOST_THRESHOLD_2 = 30 days; - uint256 public constant BOOST_THRESHOLD_3 = 90 days; - - // Events + event Staked(address indexed user, uint256 amount); event Withdrawn(address indexed user, uint256 amount); event RewardsClaimed(address indexed user, uint256 amount); - event EmergencyWithdrawn(address indexed user, uint256 amount); - /** - * @notice Initialize the contract with the LP token and reward token addresses - * @param _lpToken Address of the LP token - * @param _rewardToken Address of the reward token - * @param _rewardRate Initial reward rate per second - */ - constructor( - address _lpToken, - address _rewardToken, - uint256 _rewardRate - ) Ownable(msg.sender) { - lpToken = IERC20(_lpToken); - rewardToken = IERC20(_rewardToken); - rewardRate = _rewardRate; - lastUpdateTime = block.timestamp; + constructor(address _lpToken, address _rewardToken, uint96 _rewardRate) Ownable(msg.sender) { + (lpToken, rewardToken, rewardRate, lastUpdateTime) = (IERC20(_lpToken), IERC20(_rewardToken), _rewardRate, block.timestamp); } - function updateReward(address _user) internal { - rewardPerTokenStored = rewardPerToken(); + function calculateBoostMultiplier(address account) public view returns (uint256) { + UserInfo storage user = userInfo[account]; + return user.startTime == 0 ? 100 : + block.timestamp - user.startTime >= 90 days ? 200 : + block.timestamp - user.startTime >= 30 days ? 150 : + block.timestamp - user.startTime >= 7 days ? 125 : 100; + } + + function _rewardPerToken() internal view returns (uint256) { + return totalStaked == 0 ? rewardPerTokenStored : + rewardPerTokenStored + ((block.timestamp - lastUpdateTime) * rewardRate * 1e18) / totalStaked; + } + + function _updateReward(address account) internal { + rewardPerTokenStored = _rewardPerToken(); lastUpdateTime = block.timestamp; - - if (_user != address(0)) { - UserInfo storage user = userInfo[_user]; - user.pendingRewards = earned(_user); - user.rewardDebt = (user.amount * rewardPerTokenStored) / 1e18; + if (account != address(0)) { + UserInfo storage user = userInfo[account]; + (user.pendingRewards, user.rewardDebt) = (uint96(earned(account)), uint96((user.amount * rewardPerTokenStored) / 1e18)); } } - - function rewardPerToken() public view returns (uint256) { - if (totalStaked == 0) { - return rewardPerTokenStored; - } - - uint256 timeElapsed = block.timestamp - lastUpdateTime; - uint256 rewardAccrued = (timeElapsed * rewardRate * 1e18) / totalStaked; - return rewardPerTokenStored + rewardAccrued; + + function earned(address account) public view returns (uint256) { + UserInfo storage user = userInfo[account]; + if (user.amount == 0) return user.pendingRewards; + return (user.pendingRewards + (user.amount * (_rewardPerToken() - (user.rewardDebt / user.amount))) / 1e18) * calculateBoostMultiplier(account) / 100; } - function earned(address _user) public view returns (uint256) { - UserInfo storage user = userInfo[_user]; - if (user.amount == 0) { - return user.pendingRewards; - } - - uint256 currentRewardPerToken = rewardPerToken(); - uint256 newRewards = (user.amount * (currentRewardPerToken - user.rewardDebt / user.amount)) / 1e18; - uint256 totalRewards = user.pendingRewards + newRewards; - - uint256 multiplier = calculateBoostMultiplier(_user); - return (totalRewards * multiplier) / 100; + function pendingRewards(address account) external view returns (uint256) { return earned(account); } + + function stake(uint96 amount) external { + require(amount > 0, "Cannot stake 0"); + _updateReward(msg.sender); + UserInfo storage user = userInfo[msg.sender]; + if (user.amount == 0) user.startTime = uint32(block.timestamp); + lpToken.safeTransferFrom(msg.sender, address(this), amount); + (user.amount, totalStaked) = (user.amount + amount, totalStaked + amount); + emit Staked(msg.sender, amount); } - - /** - * @notice Stake LP tokens into the farm - * @param _amount Amount of LP tokens to stake - */ - function stake(uint256 _amount) external nonReentrant { - require(_amount > 0, "Cannot stake 0"); - - updateReward(msg.sender); - - lpToken.transferFrom(msg.sender, address(this), _amount); - + + function withdraw(uint96 amount) external { UserInfo storage user = userInfo[msg.sender]; - if (user.amount == 0) { - user.startTime = block.timestamp; - } - - user.amount += _amount; - totalStaked += _amount; - - emit Staked(msg.sender, _amount); + require(user.amount >= amount, "Insufficient balance"); + _updateReward(msg.sender); + (user.amount, totalStaked) = (user.amount - amount, totalStaked - amount); + lpToken.safeTransfer(msg.sender, amount); + emit Withdrawn(msg.sender, amount); } - - /** - * @notice Withdraw staked LP tokens - * @param _amount Amount of LP tokens to withdraw - */ - function withdraw(uint256 _amount) external nonReentrant { + + function emergencyWithdraw() external { UserInfo storage user = userInfo[msg.sender]; - require(user.amount >= _amount, "Insufficient balance"); - - updateReward(msg.sender); - - lpToken.transfer(msg.sender, _amount); - - user.amount -= _amount; - totalStaked -= _amount; - - emit Withdrawn(msg.sender, _amount); + uint256 amount = user.amount; + (user.amount, user.startTime, user.rewardDebt, user.pendingRewards, totalStaked) = (0, 0, 0, 0, totalStaked - amount); + lpToken.safeTransfer(msg.sender, amount); + emit Withdrawn(msg.sender, amount); } - - /** - * @notice Claim pending rewards - */ - function claimRewards() external nonReentrant { - updateReward(msg.sender); - + + function claimRewards() external { + _updateReward(msg.sender); UserInfo storage user = userInfo[msg.sender]; uint256 reward = user.pendingRewards; - if (reward > 0) { user.pendingRewards = 0; - rewardToken.transfer(msg.sender, reward); + rewardToken.safeTransfer(msg.sender, reward); emit RewardsClaimed(msg.sender, reward); } } - - /** - * @notice Emergency withdraw without caring about rewards - */ - function emergencyWithdraw() external nonReentrant { - UserInfo storage user = userInfo[msg.sender]; - uint256 amount = user.amount; - - totalStaked -= amount; - - // Reset user info - user.amount = 0; - user.startTime = 0; - user.rewardDebt = 0; - user.pendingRewards = 0; - - // Return LP tokens to user - lpToken.transfer(msg.sender, amount); - - emit EmergencyWithdrawn(msg.sender, amount); - } - - /** - * @notice Calculate boost multiplier based on staking duration - * @param _user Address of the user - * @return Boost multiplier (100 = 1x, 125 = 1.25x, 150 = 1.5x, 200 = 2x) - */ - function calculateBoostMultiplier( - address _user - ) public view returns (uint256) { - UserInfo storage user = userInfo[_user]; - if (user.amount == 0 || user.startTime == 0) { - return 100; // Base multiplier (1x) - } - - uint256 stakingDuration = block.timestamp - user.startTime; - - if (stakingDuration >= BOOST_THRESHOLD_3) { - return 200; // 2x multiplier - } else if (stakingDuration >= BOOST_THRESHOLD_2) { - return 150; // 1.5x multiplier - } else if (stakingDuration >= BOOST_THRESHOLD_1) { - return 125; // 1.25x multiplier - } else { - return 100; // Base multiplier (1x) - } - } - - /** - * @notice Update reward rate - * @param _newRate New reward rate per second - */ - function updateRewardRate(uint256 _newRate) external onlyOwner { - updateReward(address(0)); // Update rewards for all users before changing rate - rewardRate = _newRate; - } - - /** - * @notice View function to see pending rewards for a user - * @param _user Address of the user - * @return Pending reward amount - */ - function pendingRewards(address _user) external view returns (uint256) { - return earned(_user); + + function updateRewardRate(uint96 newRate) external onlyOwner { + _updateReward(address(0)); + rewardRate = newRate; } } \ No newline at end of file diff --git a/challenge-3-frontend/.gitignore b/challenge-3-frontend/.gitignore index fd3dbb5..2fcdec4 100644 --- a/challenge-3-frontend/.gitignore +++ b/challenge-3-frontend/.gitignore @@ -1,10 +1,14 @@ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies -/node_modules -/.pnp -.pnp.js -.yarn/install-state.gz +node_modules +.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions # testing /coverage @@ -24,9 +28,10 @@ npm-debug.log* yarn-debug.log* yarn-error.log* +.pnpm-debug.log* -# local env files -.env*.local +# env files (can opt-in for committing if needed) +.env # vercel .vercel @@ -34,3 +39,11 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +.vscode + +# Docker +postgres_data/ + +# local env files +.env*.local diff --git a/challenge-3-frontend/app/layout.tsx b/challenge-3-frontend/app/layout.tsx index db98252..f274693 100644 --- a/challenge-3-frontend/app/layout.tsx +++ b/challenge-3-frontend/app/layout.tsx @@ -1,14 +1,15 @@ import type { Metadata } from "next"; import { Unbounded } from "next/font/google"; import "./globals.css"; -import '@rainbow-me/rainbowkit/styles.css'; -import { Providers } from '@/app/providers'; +import "@rainbow-me/rainbowkit/styles.css"; +import { Providers } from "@/app/providers"; +import Navbar from "@/components/navbar"; const unbounded = Unbounded({ - subsets: ['latin'], - weight: ['400', '700'], - display: 'swap', -}) + subsets: ["latin"], + weight: ["400", "700"], + display: "swap", +}); export const metadata: Metadata = { title: "DOT UI kit", @@ -21,14 +22,11 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - - + + -
- {children} -
+ +
{children}
diff --git a/challenge-3-frontend/app/mint-redeem-lst-bifrost/page.tsx b/challenge-3-frontend/app/mint-redeem-lst-bifrost/page.tsx index aa53c21..d9f4a62 100644 --- a/challenge-3-frontend/app/mint-redeem-lst-bifrost/page.tsx +++ b/challenge-3-frontend/app/mint-redeem-lst-bifrost/page.tsx @@ -1,15 +1,29 @@ "use client"; + import MintRedeemLstBifrost from "@/components/mint-redeem-lst-bifrost"; -import SigpassKit from "@/components/sigpasskit"; -import Navbar from "@/components/navbar"; +import { + Card, + CardHeader, + CardTitle, + CardDescription, +} from "@/components/ui/card"; export default function MintRedeemLstBifrostPage() { return ( -
- - -

Mint/Redeem LST Bifrost

+
+ + + + Mint / Redeem LST Bifrost + + + Swap supported assets for liquid staking tokens or redeem them anytime through + cross-chain Bifrost orders with seamless approvals and progress tracking. + + + +
); -} +} \ No newline at end of file diff --git a/challenge-3-frontend/app/page.tsx b/challenge-3-frontend/app/page.tsx index fb01185..c36250c 100644 --- a/challenge-3-frontend/app/page.tsx +++ b/challenge-3-frontend/app/page.tsx @@ -1,108 +1,217 @@ import Image from "next/image"; import Link from "next/link"; +import { + ChevronRight, + Clock, + LockKeyhole, + ArrowUpRight, + Gift, + Users, + Shield, +} from "lucide-react"; -export default function Home() { +export default function HomePage() { return ( -
-
- OpenGuild logo -

Get started by checking out the demos

-
    -
  1. - Wallet -
  2. -
  3. - Send transaction -
  4. -
  5. - Write contract -
  6. -
  7. - Mint/Redeem LST Bifrost -
  8. -
-
- - Vercel logomark - Deploy now - - - Read our docs - +
+ {/* Hero Section */} +
+
+
+

+ Secure Token Vesting Platform +

+

+ Manage your token allocations with customizable vesting schedules. + Perfect for teams, investors, and token holders. +

+
+ + Launch Dashboard + + + Learn More + +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
-
-