From 299289733dd6e05767ab168445a196c556ce5ea6 Mon Sep 17 00:00:00 2001 From: Bobby Fiando Date: Thu, 14 Nov 2024 18:53:20 +0700 Subject: [PATCH] WIP: Eduena endowment fund contract --- remappings.txt | 1 + src/Counter.sol | 14 ----- src/EduEna.sol | 73 ----------------------- src/EduenaEndowmentFund.sol | 114 ++++++++++++++++++++++++++++++++++++ src/ScholarshipManager.sol | 85 +++++++++++++++++++++++++++ src/interfaces/IUSDe.sol | 27 +++++++++ src/mocks/MockSUSDe.sol | 10 ++++ src/mocks/MockUSDe.sol | 12 ++++ test/Counter.t.sol | 24 -------- test/EduEna.t.sol | 66 +++++++++++++++++++++ 10 files changed, 315 insertions(+), 111 deletions(-) create mode 100644 remappings.txt delete mode 100644 src/Counter.sol delete mode 100644 src/EduEna.sol create mode 100644 src/EduenaEndowmentFund.sol create mode 100644 src/ScholarshipManager.sol create mode 100644 src/interfaces/IUSDe.sol create mode 100644 src/mocks/MockSUSDe.sol create mode 100644 src/mocks/MockUSDe.sol delete mode 100644 test/Counter.t.sol create mode 100644 test/EduEna.t.sol diff --git a/remappings.txt b/remappings.txt new file mode 100644 index 0000000..2b69fdf --- /dev/null +++ b/remappings.txt @@ -0,0 +1 @@ +@openzeppelin/contracts=lib/openzeppelin-contracts/contracts \ No newline at end of file diff --git a/src/Counter.sol b/src/Counter.sol deleted file mode 100644 index aded799..0000000 --- a/src/Counter.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -contract Counter { - uint256 public number; - - function setNumber(uint256 newNumber) public { - number = newNumber; - } - - function increment() public { - number++; - } -} diff --git a/src/EduEna.sol b/src/EduEna.sol deleted file mode 100644 index 39560b0..0000000 --- a/src/EduEna.sol +++ /dev/null @@ -1,73 +0,0 @@ -pragma solidity ^0.8.13; - -contract EduEna { - - struct Donor { - string name; - uint256 amountDonated; - } - - struct Recipient { - string name; - uint256 fundsAllocated; - bool hasReceivedFunds; - } - - address public ngo; - uint256 public totalFunds; - - mapping(address => Donor) public donors; - mapping(address => Recipient) public recipients; - - // Events - event DonationReceived(address indexed donor, uint256 amount); - event FundsAllocated(address indexed recipient, uint256 amount); - event FundsDisbursed(address indexed recipient, uint256 amount); - - modifier onlyNGO() { - require(msg.sender == ngo, "Only the NGO can call this function"); - _; - } - - constructor() { - ngo = msg.sender; - } - - function donate(string memory donorName) public payable { - require(msg.value > 0, "Donation amount must be greater than 0"); - - Donor storage donor = donors[msg.sender]; - donor.name = donorName; - donor.amountDonated += msg.value; - - totalFunds += msg.value; - emit DonationReceived(msg.sender, msg.value); - } - - function allocationFunds(address recipientAddress, string memory recipientName, uint256 amount) public onlyNGO { - require(totalFunds >= amount, "Insufficient funds"); - - Recipient storage recipient = recipients[recipientAddress]; - recipient.name = recipientName; - recipient.fundsAllocated += amount; - recipient.hasReceivedFunds = false; - - totalFunds -= amount; - emit FundsAllocated(recipientAddress, amount); - } - - function disburseFunds(address recipientAddress) external onlyNGO { - Recipient storage recipient = recipients[recipientAddress]; - require(recipient.fundsAllocated > 0, "No funds allocated to this recipient"); - require(!recipient.hasReceivedFunds, "Funds already disbursed to this recipient"); - - recipient.hasReceivedFunds = true; - payable(recipientAddress).transfer(recipient.fundsAllocated); - emit FundsDisbursed(recipientAddress, recipient.fundsAllocated); - } - - function getContractBalance() public view returns (uint256) { - return address(this).balance; - } - -} \ No newline at end of file diff --git a/src/EduenaEndowmentFund.sol b/src/EduenaEndowmentFund.sol new file mode 100644 index 0000000..29e7c9d --- /dev/null +++ b/src/EduenaEndowmentFund.sol @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import "./interfaces/IUSDe.sol"; +import "forge-std/console.sol"; //NOTE: testing only + +error InsufficientShares(); +error InsufficientFunds(); +error InsufficientBalance(); +error OnlyOwner(); +error DepositAmountZero(); +error InsufficientYield(); + +contract EduenaEndowmentFund is ReentrancyGuard { + using SafeERC20 for IERC20; + + address public owner; + ISUSDe public sUSDe; + IERC20 public USDe; + uint256 public totalShares; + uint256 public lastAssetValueInUSDe; + uint256 public totalUnclaimedYield; + mapping(address => uint256) public donorShares; + + event Deposit(address indexed donor, uint256 amount); + event Stake(uint256 amount); + event Withdraw(address indexed recipient, uint256 amount); + event YieldUpdated(uint256 newAssetValueInUSDe, uint256 yield); + + constructor(address _USDeAddress, address _sUSDeAddress) { + owner = msg.sender; + USDe = IERC20(_USDeAddress); + sUSDe = ISUSDe(_sUSDeAddress); + } + + function poolBalance() public view returns (uint256) { + return USDe.balanceOf(address(this)); + } + + function deposit(uint256 amount) external nonReentrant { + if (amount == 0) revert DepositAmountZero(); + + USDe.safeTransferFrom(msg.sender, address(this), amount); + + uint256 shares; + if (totalShares == 0) { + shares = amount; + } else { + shares = (amount * totalShares) / lastAssetValueInUSDe; + } + + donorShares[msg.sender] += shares; + totalShares += shares; + emit Deposit(msg.sender, amount); + + _stake(amount); + updateYield(); + } + + function _stake(uint256 amount) internal { + USDe.approve(address(sUSDe), amount); + sUSDe.deposit(amount, address(this)); + emit Stake(amount); + } + + function withdraw(uint256 amount) external nonReentrant { + uint256 shares = (amount * totalShares) / lastAssetValueInUSDe; + if (donorShares[msg.sender] < shares) revert InsufficientShares(); + + uint256 previewAmount = sUSDe.previewRedeem(shares); + if (previewAmount < amount) revert InsufficientFunds(); + + donorShares[msg.sender] -= shares; + totalShares -= shares; + + //FIXME: Fix the withdraw to the donor as a sUSDe + sUSDe.redeem(shares, msg.sender, address(this)); + emit Withdraw(msg.sender, previewAmount); + updateYield(); + } + + //TODO: Implement the distribute function to distribute the returns to eligible students verified by scholarships creator + //TODO: Use scholarship manager to handle the scholarship creation and verification + function distribute(address payable student, uint256 amount) external { + if (msg.sender != owner) revert OnlyOwner(); + if (amount > totalUnclaimedYield) revert InsufficientYield(); + + uint256 sUSDeBalance = sUSDe.balanceOf(address(this)); + if (amount > sUSDeBalance) revert InsufficientBalance(); + + uint256 previewAmount = sUSDe.previewRedeem(amount); + // sUSDe.redeem(amount); + USDe.safeTransfer(student, previewAmount); + totalUnclaimedYield -= amount; + + emit Withdraw(student, previewAmount); + updateYield(); + } + + function updateYield() public { + //todo: Implement the yield calculation + uint256 sUSDeBalance = sUSDe.balanceOf(address(this)); + + uint256 assetValueInUSDe = sUSDe.previewDeposit(sUSDeBalance); + lastAssetValueInUSDe = assetValueInUSDe; + + uint256 yield = assetValueInUSDe - lastAssetValueInUSDe; + totalUnclaimedYield += yield; + emit YieldUpdated(assetValueInUSDe, yield); + } +} diff --git a/src/ScholarshipManager.sol b/src/ScholarshipManager.sol new file mode 100644 index 0000000..d6ef4dc --- /dev/null +++ b/src/ScholarshipManager.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +contract ScholarshipManager { + address public owner; + uint256 public scholarshipCount; + + struct Scholarship { + uint256 id; + uint256 amount; + string criteria; + address[] applicants; + mapping(address => bool) verifiedApplicants; + mapping(address => bool) claimed; + } + + mapping(uint256 => Scholarship) public scholarships; + + event ScholarshipCreated(uint256 indexed scholarshipId, uint256 amount, string criteria); + event ScholarshipUpdated(uint256 indexed scholarshipId, uint256 amount, string criteria); + event ScholarshipApplied(uint256 indexed scholarshipId, address indexed applicant); + event ApplicantVerified(uint256 indexed scholarshipId, address indexed applicant); + event ScholarshipClaimed(uint256 indexed scholarshipId, address indexed applicant); + + modifier onlyOwner() { + require(msg.sender == owner, "Only owner can call this function"); + _; + } + + constructor() { + owner = msg.sender; + } + + function createScholarship(uint256 amount, string memory criteria) external onlyOwner { + scholarshipCount++; + Scholarship storage scholarship = scholarships[scholarshipCount]; + scholarship.id = scholarshipCount; + scholarship.amount = amount; + scholarship.criteria = criteria; + + emit ScholarshipCreated(scholarshipCount, amount, criteria); + } + + function updateScholarship(uint256 scholarshipId, uint256 amount, string memory criteria) external onlyOwner { + Scholarship storage scholarship = scholarships[scholarshipId]; + scholarship.amount = amount; + scholarship.criteria = criteria; + + emit ScholarshipUpdated(scholarshipId, amount, criteria); + } + + function getScholarshipDetails(uint256 scholarshipId) external view returns (uint256, uint256, string memory, address[] memory) { + Scholarship storage scholarship = scholarships[scholarshipId]; + return (scholarship.id, scholarship.amount, scholarship.criteria, scholarship.applicants); + } + + function applyForScholarship(uint256 scholarshipId) external { + Scholarship storage scholarship = scholarships[scholarshipId]; + scholarship.applicants.push(msg.sender); + + emit ScholarshipApplied(scholarshipId, msg.sender); + } + + function verifyApplicant(address applicant, uint256 scholarshipId) external onlyOwner { + Scholarship storage scholarship = scholarships[scholarshipId]; + scholarship.verifiedApplicants[applicant] = true; + + emit ApplicantVerified(scholarshipId, applicant); + } + + function getApplicantStatus(address applicant, uint256 scholarshipId) external view returns (bool) { + Scholarship storage scholarship = scholarships[scholarshipId]; + return scholarship.verifiedApplicants[applicant]; + } + + function claimScholarship(uint256 scholarshipId) external { + Scholarship storage scholarship = scholarships[scholarshipId]; + require(scholarship.verifiedApplicants[msg.sender], "Applicant not verified"); + require(!scholarship.claimed[msg.sender], "Scholarship already claimed"); + + scholarship.claimed[msg.sender] = true; + + emit ScholarshipClaimed(scholarshipId, msg.sender); + } +} \ No newline at end of file diff --git a/src/interfaces/IUSDe.sol b/src/interfaces/IUSDe.sol new file mode 100644 index 0000000..c753ff1 --- /dev/null +++ b/src/interfaces/IUSDe.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +interface ISUSDe { + function approve(address spender, uint256 amount) external; + + function transfer( + address to, + uint256 amount + ) external returns (uint256); + + function deposit( + uint256 assets, + address receiver + ) external returns (uint256); + + function redeem( + uint256 shares, + address receiver, + address _owner + ) external returns (uint256); + + function previewRedeem(uint256 shares) external view returns (uint256); + function previewDeposit(uint256 assets) external view returns (uint256); + + function balanceOf(address account) external view returns (uint256); +} diff --git a/src/mocks/MockSUSDe.sol b/src/mocks/MockSUSDe.sol new file mode 100644 index 0000000..2683328 --- /dev/null +++ b/src/mocks/MockSUSDe.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; + +contract MockSUSDe is ERC4626 { + constructor( + ERC20 _usdeToken + ) ERC4626(_usdeToken) ERC20("Mock Staked USDe", "MSUSDe") {} +} \ No newline at end of file diff --git a/src/mocks/MockUSDe.sol b/src/mocks/MockUSDe.sol new file mode 100644 index 0000000..6cea707 --- /dev/null +++ b/src/mocks/MockUSDe.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract MockUSDe is ERC20 { + constructor() ERC20("Mock USDe", "mUSDe") {} + + function mint(address to, uint256 amount) external { + _mint(to, amount); + } +} \ No newline at end of file diff --git a/test/Counter.t.sol b/test/Counter.t.sol deleted file mode 100644 index 54b724f..0000000 --- a/test/Counter.t.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Test, console} from "forge-std/Test.sol"; -import {Counter} from "../src/Counter.sol"; - -contract CounterTest is Test { - Counter public counter; - - function setUp() public { - counter = new Counter(); - counter.setNumber(0); - } - - function test_Increment() public { - counter.increment(); - assertEq(counter.number(), 1); - } - - function testFuzz_SetNumber(uint256 x) public { - counter.setNumber(x); - assertEq(counter.number(), x); - } -} diff --git a/test/EduEna.t.sol b/test/EduEna.t.sol new file mode 100644 index 0000000..3c0c78f --- /dev/null +++ b/test/EduEna.t.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; +import "../src/EduEna.sol"; +import "../src/mocks/MockUSDe.sol"; +import "../src/mocks/MockSUSDe.sol"; + +contract EduEnaTest is Test { + EduEna eduEna; + MockUSDe USDe; + MockSUSDe sUSDe; + address owner; + address donor; + + function setUp() public { + owner = address(this); + donor = address(0x123); + + USDe = new MockUSDe(); + sUSDe = new MockSUSDe(USDe); + eduEna = new EduEna(address(USDe), address(sUSDe)); + + USDe.mint(donor, 1000 ether); + } + + function testDeposit() public { + uint256 amount = 100 ether; + + vm.startPrank(donor); + USDe.approve(address(eduEna), amount); + eduEna.deposit(amount); + vm.stopPrank(); + + assertEq(eduEna.donorShares(donor), amount); + assertEq(USDe.balanceOf(address(eduEna)), 0); + assertEq(sUSDe.balanceOf(address(eduEna)), amount); + } + + function testWithdraw() public { + uint256 depositAmount = 100 ether; + uint256 withdrawAmount = 50 ether; + + vm.startPrank(donor); + USDe.approve(address(eduEna), depositAmount); + eduEna.deposit(depositAmount); + vm.stopPrank(); + + vm.startPrank(donor); + uint256 donorBalance = USDe.balanceOf(donor); + eduEna.withdraw(withdrawAmount); + vm.stopPrank(); + + assertEq(eduEna.donorShares(donor), depositAmount - withdrawAmount); + //FIXME: assert sUSDe balance of donor, instead of USDe balance of donor + assertEq(USDe.balanceOf(donor), withdrawAmount + donorBalance); + assertEq(sUSDe.balanceOf(address(eduEna)), depositAmount - withdrawAmount); + + console.log("sUSDe balance of donor: %s", sUSDe.balanceOf(donor)); + console.log("USDe balance of donor: %s", USDe.balanceOf(donor)); + console.log( + "sUSDe balance of eduEna contract: %s", + sUSDe.balanceOf(address(eduEna)) + ); + } +}