diff --git a/script/Counter.s.sol b/script/Counter.s.sol deleted file mode 100644 index cdc1fe9..0000000 --- a/script/Counter.s.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Script, console} from "forge-std/Script.sol"; -import {Counter} from "../src/Counter.sol"; - -contract CounterScript is Script { - Counter public counter; - - function setUp() public {} - - function run() public { - vm.startBroadcast(); - - counter = new Counter(); - - vm.stopBroadcast(); - } -} diff --git a/src/Eduena.sol b/src/Eduena.sol new file mode 100644 index 0000000..ee01f85 --- /dev/null +++ b/src/Eduena.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20.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 InsufficientBalance(); +error OnlyOwner(); +error DepositAmountZero(); +error InsufficientYield(); +error ExceedsUnclaimedYield(); +error NotEligibleForScholarship(); + +contract Eduena is ERC20, ReentrancyGuard { + using SafeERC20 for IERC20; + + address public owner; + ISUSDe public sUSDe; + IERC20 public USDe; + uint256 public lastAssetValueInUSDe; + uint256 public totalUnclaimedYield; + + 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 _USDe, address _sUSDe) ERC20("Eduena", "EDN") { + owner = msg.sender; + USDe = IERC20(_USDe); + sUSDe = ISUSDe(_sUSDe); + } + + function deposit(uint256 amount) external nonReentrant { + if (amount == 0) revert DepositAmountZero(); + + USDe.safeTransferFrom(msg.sender, address(this), amount); + uint256 shares = _calculateShares(amount); + _mint(msg.sender, 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 { + _withdraw(msg.sender, amount); + } + + function _withdraw(address recipient, uint256 amount) private { + uint256 shares = _calculateShares(amount); + uint256 previewAmount = sUSDe.previewRedeem(shares); + + _burn(msg.sender, shares); + sUSDe.transfer(recipient, amount); + emit Withdraw(recipient, previewAmount); + updateYield(); + } + + // This function is a simulation to demonstrate the distribution of scholarships to students. + function distribute( + address recipient, + uint256 amount + ) external nonReentrant { + bool isEligible = checkEligibility(recipient); + if (!isEligible) revert NotEligibleForScholarship(); + + if (amount > totalUnclaimedYield) revert ExceedsUnclaimedYield(); + + uint256 sUSDeBalance = sUSDe.balanceOf(address(this)); + if (amount > sUSDeBalance) revert InsufficientBalance(); + + _withdraw(recipient, amount); + } + + function checkEligibility(address student) internal pure returns (bool) { + return true; + } + + //FIXME: Fix the logic of this function + function updateYield() public { + uint256 sUSDeBalance = sUSDe.balanceOf(address(this)); + uint256 assetValueInUSDe = sUSDe.previewMint(sUSDeBalance); + + if (lastAssetValueInUSDe == 0) { + lastAssetValueInUSDe = sUSDe.balanceOf(address(this)); + } + + if (lastAssetValueInUSDe > 0) { + uint256 yield = assetValueInUSDe - lastAssetValueInUSDe; + uint256 shares = _calculateShares(yield); + _mint(address(this), shares); + + totalUnclaimedYield += yield; + emit YieldUpdated(assetValueInUSDe, yield); + } + } + + function _calculateShares(uint256 amount) internal view returns (uint256) { + if (totalSupply() == 0) { + return amount; + } else { + return (amount * totalSupply()) / lastAssetValueInUSDe; + } + } +} diff --git a/src/EduenaEndowmentFund.sol b/src/EduenaEndowmentFund.sol deleted file mode 100644 index 29e7c9d..0000000 --- a/src/EduenaEndowmentFund.sol +++ /dev/null @@ -1,114 +0,0 @@ -// 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/interfaces/IUSDe.sol b/src/interfaces/IUSDe.sol index c753ff1..31d0ac1 100644 --- a/src/interfaces/IUSDe.sol +++ b/src/interfaces/IUSDe.sol @@ -21,7 +21,8 @@ interface ISUSDe { ) external returns (uint256); function previewRedeem(uint256 shares) external view returns (uint256); - function previewDeposit(uint256 assets) external view returns (uint256); + + function previewMint(uint256 shares) external view returns (uint256); function balanceOf(address account) external view returns (uint256); } diff --git a/src/mocks/MockSUSDe.sol b/src/mocks/MockSUSDe.sol index 2683328..716ef3c 100644 --- a/src/mocks/MockSUSDe.sol +++ b/src/mocks/MockSUSDe.sol @@ -7,4 +7,8 @@ contract MockSUSDe is ERC4626 { constructor( ERC20 _usdeToken ) ERC4626(_usdeToken) ERC20("Mock Staked USDe", "MSUSDe") {} -} \ No newline at end of file + + function mint(address to, uint256 amount) public { + _mint(to, amount); + } +} diff --git a/test/EduEna.t.sol b/test/EduEna.t.sol index 3c0c78f..1a5b0a3 100644 --- a/test/EduEna.t.sol +++ b/test/EduEna.t.sol @@ -2,65 +2,94 @@ pragma solidity ^0.8.0; import "forge-std/Test.sol"; -import "../src/EduEna.sol"; +import "../src/Eduena.sol"; import "../src/mocks/MockUSDe.sol"; import "../src/mocks/MockSUSDe.sol"; -contract EduEnaTest is Test { - EduEna eduEna; +contract EduenaTest is Test { + Eduena eduena; MockUSDe USDe; MockSUSDe sUSDe; address owner; address donor; + address donor2; function setUp() public { owner = address(this); donor = address(0x123); + donor2 = address(0x456); USDe = new MockUSDe(); sUSDe = new MockSUSDe(USDe); - eduEna = new EduEna(address(USDe), address(sUSDe)); + eduena = new Eduena(address(USDe), address(sUSDe)); USDe.mint(donor, 1000 ether); + USDe.mint(donor2, 1000 ether); } function testDeposit() public { uint256 amount = 100 ether; vm.startPrank(donor); - USDe.approve(address(eduEna), amount); - eduEna.deposit(amount); + 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); + assertEq(USDe.balanceOf(address(eduena)), 0); + assertEq(sUSDe.balanceOf(address(eduena)), amount); + assertEq(sUSDe.totalSupply(), amount); + + //TODO: test yield, increasing the value of the asset and donor2 depositing + + vm.startPrank(donor2); + USDe.approve(address(eduena), amount); + eduena.deposit(amount); + vm.stopPrank(); + + // assertEq(USDe.balanceOf(address(eduena)), 0); + // assertEq(sUSDe.balanceOf(address(eduena)), amount * 2); + // assertEq(sUSDe.totalSupply(), amount * 2); + + // console.log(eduena.totalSupply()); + // console.log(eduena.lastAssetValueInUSDe()); + // console.log(eduena.totalUnclaimedYield()); } function testWithdraw() public { uint256 depositAmount = 100 ether; - uint256 withdrawAmount = 50 ether; + uint256 withdrawAmount = 100 ether; vm.startPrank(donor); - USDe.approve(address(eduEna), depositAmount); - eduEna.deposit(depositAmount); + USDe.approve(address(eduena), depositAmount); + eduena.deposit(depositAmount); vm.stopPrank(); vm.startPrank(donor); - uint256 donorBalance = USDe.balanceOf(donor); - eduEna.withdraw(withdrawAmount); + 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)) + assertEq(USDe.balanceOf(donor), 900 ether); + assertEq( + sUSDe.balanceOf(address(eduena)), + depositAmount - withdrawAmount ); + assertEq(sUSDe.balanceOf(donor), 100 ether); + assertEq(eduena.totalUnclaimedYield(), 0); + assertEq(eduena.lastAssetValueInUSDe(), 0 ether); + } + + function testDistributeScholarship() public { + // uint256 depositAmount = 100 ether; + // address student = address(0x123); + // vm.startPrank(donor); + // USDe.approve(address(eduena), depositAmount); + // eduena.deposit(depositAmount); + // vm.stopPrank(); + // vm.startPrank(donor); + // eduena.distributeScholarship(student, depositAmount); + // vm.stopPrank(); + // assertEq(USDe.balanceOf(student), depositAmount); + // assertEq(sUSDe.balanceOf(address(eduena)), 0); + // assertEq(sUSDe.totalSupply(), 0); } }