├── .github └── workflows │ └── security.yaml ├── .gitignore ├── .vscode └── settings.json ├── README.md ├── brownie-config.yaml ├── contracts ├── Dai.sol ├── DeSchool.sol ├── ERC20.sol ├── LearningCurve.sol ├── PRBMath.sol ├── PRBMathUD60x18.sol ├── SafeTransferLib.sol └── interfaces │ ├── IERC20Permit.sol │ ├── I_LearningCurve.sol │ ├── I_Registry.sol │ └── I_Vault.sol ├── package.json ├── requirements-dev.txt ├── scripts ├── deploy.py └── flatten_contracts.py ├── security ├── flattener-run.sh └── slither-config.json ├── tests-mainnet ├── conftest.py ├── constants_mainnet.py ├── test_mainnet_ds.py └── test_operation_mainnet.py └── tests ├── conftest.py ├── constants_unit.py ├── test_operation_unit.py ├── test_unit_ds.py └── test_unit_lc.py /.github/workflows/security.yaml: -------------------------------------------------------------------------------- 1 | name: Security-checks 2 | 3 | on: 4 | push: 5 | branches: [ development ] 6 | pull_request: 7 | 8 | jobs: 9 | main_job: 10 | runs-on: ubuntu-latest 11 | name: Solidity Security 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | - uses: actions/setup-node@v1.4.4 16 | with: 17 | node-version: '12' 18 | - uses: actions/setup-python@v2 19 | with: 20 | python-version: '3.8' 21 | - name: Prepare environment 22 | run: | 23 | npm install -g ganache-cli@6.12.1 24 | pip3 install solc-select 25 | solc-select install 0.8.4 26 | solc-select use 0.8.4 27 | pip3 install slither-analyzer 28 | pip install -r requirements-dev.txt 29 | - name: Prepare contracts 30 | shell: bash 31 | run: | 32 | npm run sec:flatten 33 | rm package.json 34 | - name: Slither Static Analysis 35 | uses: luisfontes19/slither-static-analysis-action@v0.3.2 36 | with: 37 | slither-version: '0.6.13' 38 | run-npm-install: true 39 | high-threshold: 0 40 | medium-threshold: 21 41 | low-threshold: 30 42 | optimization-threshold: 999 43 | informative-threshold: 999 44 | projectPath: "./flattened" 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /reports 3 | /.idea 4 | /.pytest_cache 5 | /tests/__pycache__ 6 | /tests-mainnet/__pycache__ 7 | /scripts/__pycache__ 8 | 9 | package-lock.json 10 | node_modules -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "solidity.compileUsingRemoteVersion": "v0.8.13+commit.abaa5c0e" 3 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The Kernel Learning Curve 2 | 3 | Smart contracts for free and continuous online learning environments, which nevertheless ensure that course designers are properly rewarded for their work. 4 | 5 | ## Testing and Development 6 | 7 | This repository uses brownie for testing. 8 | ### Dependencies 9 | 10 | * [python3](https://www.python.org/downloads/release/python-368/) version 3.6 or greater, python3-dev 11 | * [brownie](https://github.com/iamdefinitelyahuman/brownie) - tested with version [1.14.6](https://github.com/eth-brownie/brownie/releases/tag/v1.14.6) 12 | * [ganache-cli](https://github.com/trufflesuite/ganache-cli) - tested with version [6.12.2](https://github.com/trufflesuite/ganache-cli/releases/tag/v6.12.2) 13 | 14 | 15 | To run the non-mainnet test suite (no yield functionality) 16 | 17 | ``` 18 | cd learning-curve 19 | brownie test tests -s 20 | ``` 21 | 22 | To run the mainnet test suite (yield functionality): 23 | 1. Add a WEB3_INFURA_PROJECT_ID as an [environmental variable](https://eth-brownie.readthedocs.io/en/stable/network-management.html#using-infura) 24 | 2. Add an ETHERSCAN_TOKEN as an environmental variable 25 | 3. Run the following 26 | ``` 27 | cd learning-curve 28 | brownie test tests-mainnet --network=mainnet-fork -s 29 | ``` 30 | 31 | ## Current gas report 32 | ``` 33 | DeSchool 34 | ├─ constructor - avg: 2722866 avg (confirmed): 2722866 low: 2722866 high: 2722866 35 | ├─ permitAndRegister - avg: 146645 avg (confirmed): 146645 low: 146645 high: 146645 36 | ├─ createCourse - avg: 119368 avg (confirmed): 123066 low: 23221 high: 134617 37 | ├─ mint - avg: 90247 avg (confirmed): 113111 low: 22621 high: 116626 38 | ├─ register - avg: 59684 avg (confirmed): 62112 low: 22511 high: 86669 39 | └─ redeem - avg: 57139 avg (confirmed): 68031 low: 22564 high: 70531 40 | LearningCurve 41 | ├─ constructor - avg: 1866796 avg (confirmed): 1866796 low: 1866796 high: 1866796 42 | ├─ permitAndMint - avg: 122563 avg (confirmed): 122563 low: 122563 high: 122563 43 | ├─ initialise - avg: 119427 avg (confirmed): 132872 low: 22214 high: 132872 44 | ├─ burn - avg: 65850 avg (confirmed): 65850 low: 65642 high: 66346 45 | ├─ mint - avg: 61697 avg (confirmed): 61697 low: 61356 high: 62166 46 | └─ approve - avg: 44103 avg (confirmed): 44103 low: 44101 high: 44113 47 | ``` 48 | -------------------------------------------------------------------------------- /brownie-config.yaml: -------------------------------------------------------------------------------- 1 | name: "learning-curve" -------------------------------------------------------------------------------- /contracts/Dai.sol: -------------------------------------------------------------------------------- 1 | 2 | pragma solidity =0.5.12; 3 | 4 | ////// /nix/store/8xb41r4qd0cjb63wcrxf1qmfg88p0961-dss-6fd7de0/src/lib.sol 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | 18 | /* pragma solidity 0.5.12; */ 19 | 20 | contract LibNote { 21 | event LogNote( 22 | bytes4 indexed sig, 23 | address indexed usr, 24 | bytes32 indexed arg1, 25 | bytes32 indexed arg2, 26 | bytes data 27 | ) anonymous; 28 | 29 | modifier note { 30 | _; 31 | assembly { 32 | // log an 'anonymous' event with a constant 6 words of calldata 33 | // and four indexed topics: selector, caller, arg1 and arg2 34 | let mark := msize // end of memory ensures zero 35 | mstore(0x40, add(mark, 288)) // update free memory pointer 36 | mstore(mark, 0x20) // bytes type data offset 37 | mstore(add(mark, 0x20), 224) // bytes size (padded) 38 | calldatacopy(add(mark, 0x40), 0, 224) // bytes payload 39 | log4(mark, 288, // calldata 40 | shl(224, shr(224, calldataload(0))), // msg.sig 41 | caller, // msg.sender 42 | calldataload(4), // arg1 43 | calldataload(36) // arg2 44 | ) 45 | } 46 | } 47 | } 48 | 49 | ////// /nix/store/8xb41r4qd0cjb63wcrxf1qmfg88p0961-dss-6fd7de0/src/dai.sol 50 | // Copyright (C) 2017, 2018, 2019 dbrock, rain, mrchico 51 | 52 | // This program is free software: you can redistribute it and/or modify 53 | // it under the terms of the GNU Affero General Public License as published by 54 | // the Free Software Foundation, either version 3 of the License, or 55 | // (at your option) any later version. 56 | // 57 | // This program is distributed in the hope that it will be useful, 58 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 59 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 60 | // GNU Affero General Public License for more details. 61 | // 62 | // You should have received a copy of the GNU Affero General Public License 63 | // along with this program. If not, see . 64 | 65 | /* pragma solidity 0.5.12; */ 66 | 67 | /* import "./lib.sol"; */ 68 | 69 | contract Dai is LibNote { 70 | // --- Auth --- 71 | mapping (address => uint) public wards; 72 | function rely(address guy) external note auth { wards[guy] = 1; } 73 | function deny(address guy) external note auth { wards[guy] = 0; } 74 | modifier auth { 75 | require(wards[msg.sender] == 1, "Dai/not-authorized"); 76 | _; 77 | } 78 | 79 | // --- ERC20 Data --- 80 | string public constant name = "Dai Stablecoin"; 81 | string public constant symbol = "DAI"; 82 | string public constant version = "1"; 83 | uint8 public constant decimals = 18; 84 | uint256 public totalSupply; 85 | 86 | mapping (address => uint) public balanceOf; 87 | mapping (address => mapping (address => uint)) public allowance; 88 | mapping (address => uint) public nonces; 89 | 90 | event Approval(address indexed src, address indexed guy, uint wad); 91 | event Transfer(address indexed src, address indexed dst, uint wad); 92 | 93 | // --- Math --- 94 | function add(uint x, uint y) internal pure returns (uint z) { 95 | require((z = x + y) >= x); 96 | } 97 | function sub(uint x, uint y) internal pure returns (uint z) { 98 | require((z = x - y) <= x); 99 | } 100 | 101 | // --- EIP712 niceties --- 102 | bytes32 public DOMAIN_SEPARATOR; 103 | // bytes32 public constant PERMIT_TYPEHASH = keccak256("Permit(address holder,address spender,uint256 nonce,uint256 expiry,bool allowed)"); 104 | bytes32 public constant PERMIT_TYPEHASH = 0xea2aa0a1be11a07ed86d755c93467f4f82362b452371d1ba94d1715123511acb; 105 | 106 | constructor(uint256 chainId_) public { 107 | wards[msg.sender] = 1; 108 | DOMAIN_SEPARATOR = keccak256(abi.encode( 109 | keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), 110 | keccak256(bytes(name)), 111 | keccak256(bytes(version)), 112 | chainId_, 113 | address(this) 114 | )); 115 | } 116 | 117 | // --- Token --- 118 | function transfer(address dst, uint wad) external returns (bool) { 119 | return transferFrom(msg.sender, dst, wad); 120 | } 121 | function transferFrom(address src, address dst, uint wad) 122 | public returns (bool) 123 | { 124 | require(balanceOf[src] >= wad, "Dai/insufficient-balance"); 125 | if (src != msg.sender && allowance[src][msg.sender] != uint(-1)) { 126 | require(allowance[src][msg.sender] >= wad, "Dai/insufficient-allowance"); 127 | allowance[src][msg.sender] = sub(allowance[src][msg.sender], wad); 128 | } 129 | balanceOf[src] = sub(balanceOf[src], wad); 130 | balanceOf[dst] = add(balanceOf[dst], wad); 131 | emit Transfer(src, dst, wad); 132 | return true; 133 | } 134 | function mint(address usr, uint wad) external auth { 135 | balanceOf[usr] = add(balanceOf[usr], wad); 136 | totalSupply = add(totalSupply, wad); 137 | emit Transfer(address(0), usr, wad); 138 | } 139 | function burn(address usr, uint wad) external { 140 | require(balanceOf[usr] >= wad, "Dai/insufficient-balance"); 141 | if (usr != msg.sender && allowance[usr][msg.sender] != uint(-1)) { 142 | require(allowance[usr][msg.sender] >= wad, "Dai/insufficient-allowance"); 143 | allowance[usr][msg.sender] = sub(allowance[usr][msg.sender], wad); 144 | } 145 | balanceOf[usr] = sub(balanceOf[usr], wad); 146 | totalSupply = sub(totalSupply, wad); 147 | emit Transfer(usr, address(0), wad); 148 | } 149 | function approve(address usr, uint wad) external returns (bool) { 150 | allowance[msg.sender][usr] = wad; 151 | emit Approval(msg.sender, usr, wad); 152 | return true; 153 | } 154 | 155 | // --- Alias --- 156 | function push(address usr, uint wad) external { 157 | transferFrom(msg.sender, usr, wad); 158 | } 159 | function pull(address usr, uint wad) external { 160 | transferFrom(usr, msg.sender, wad); 161 | } 162 | function move(address src, address dst, uint wad) external { 163 | transferFrom(src, dst, wad); 164 | } 165 | 166 | // --- Approve by signature --- 167 | function permit(address holder, address spender, uint256 nonce, uint256 expiry, 168 | bool allowed, uint8 v, bytes32 r, bytes32 s) external 169 | { 170 | bytes32 digest = 171 | keccak256(abi.encodePacked( 172 | "\x19\x01", 173 | DOMAIN_SEPARATOR, 174 | keccak256(abi.encode(PERMIT_TYPEHASH, 175 | holder, 176 | spender, 177 | nonce, 178 | expiry, 179 | allowed)) 180 | )); 181 | 182 | require(holder != address(0), "Dai/invalid-address-0"); 183 | require(holder == ecrecover(digest, v, r, s), "Dai/invalid-permit"); 184 | require(expiry == 0 || now <= expiry, "Dai/permit-expired"); 185 | require(nonce == nonces[holder]++, "Dai/invalid-nonce"); 186 | uint wad = allowed ? uint(-1) : 0; 187 | allowance[holder][spender] = wad; 188 | emit Approval(holder, spender, wad); 189 | } 190 | } -------------------------------------------------------------------------------- /contracts/DeSchool.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MPL-2.0 2 | pragma solidity 0.8.13; 3 | 4 | import "./ERC20.sol"; 5 | import "./SafeTransferLib.sol"; 6 | import "./interfaces/I_Vault.sol"; 7 | import "./interfaces/I_Registry.sol"; 8 | import "./interfaces/IERC20Permit.sol"; 9 | import "./interfaces/I_LearningCurve.sol"; 10 | 11 | /** 12 | * @title DeSchool 13 | * @author kjr217, cryptowanderer 14 | * @notice Deploys new courses and interacts with the learning curve directly to mint LEARN. 15 | */ 16 | 17 | contract DeSchool { 18 | 19 | struct Course { 20 | uint256 stake; // an amount in DAI to be staked for the duration course 21 | uint256 duration; // the duration of the course, in number of blocks 22 | string url; // url containing course data 23 | address creator; // address to receive any yield from a redeem call 24 | uint256 scholars; // keep track of how many scholars are registered so we can deregister them later 25 | uint256 completedScholars; // keep track of how many scholars have completed the course 26 | uint256 scholarshipTotal; // the total amount of DAI provided for scholarships for this course 27 | address scholarshipVault; // one scholarship vault per course, any new scholarships are simply added to it 28 | uint256 scholarshipYTokens; // the yTokens, earned by the course creator from scholarships 29 | } 30 | 31 | struct Scholar { 32 | uint256 blockRegistered; // used to create perpetual scholarships as needed 33 | } 34 | 35 | struct Learner { 36 | uint256 blockRegistered; // used to decide when a learner can claim their stake back 37 | uint256 yieldBatchId; // the batch id for this learner's Yield bearing deposit 38 | } 39 | 40 | // containing course data mapped by a courseId 41 | mapping(uint256 => Course) public courses; 42 | 43 | // containing learner data mapped by a courseId and address 44 | mapping(uint256 => mapping(address => Learner)) learnerData; 45 | // containing scholar data mapped by a courseId and the number of "completed" scholars. 46 | // We keep track of this separately from the number of scholars in order to lookup the "active" scholar 47 | // who registered most long ago, check if the course duration has passed has passed since they registered 48 | // and, if it has, we replace them with a new scholar 49 | mapping(uint256 => mapping(uint256 => Scholar)) scholarData; 50 | // containing scholarship provider amount mapped by courseId and address 51 | mapping(uint256 => mapping(address => uint256)) providerAmount; 52 | // containg currentScholar data mapped by a courseId and address for the require in registerScholar() 53 | mapping(uint256 => mapping(address => Scholar)) registered; 54 | 55 | // containing the total underlying amount for a yield batch mapped by batchId 56 | mapping(uint256 => uint256) batchTotal; 57 | // containing the total amount of yield token for a yield batch mapped by batchId 58 | mapping(uint256 => uint256) batchYieldTotal; 59 | // containing the vault address of the the yield token for a yield batch mapped by batchId 60 | mapping(uint256 => address) batchYieldAddress; 61 | 62 | // yield rewards for an eligible address 63 | mapping(address => uint256) yieldRewards; 64 | 65 | // tracker for the courseId, current represents the id of the next course 66 | uint256 private courseIdTracker; 67 | // tracker for the batchId, current represents the current batch 68 | uint256 private batchIdTracker; 69 | 70 | // the stablecoin used by the contract, DAI 71 | ERC20 public immutable stable; 72 | // the yearn registry used by the contract, to determine what the yDai address is. 73 | I_Registry public immutable registry; 74 | // interface for the learning curve 75 | I_LearningCurve public immutable learningCurve; 76 | 77 | event CourseCreated( 78 | uint256 indexed courseId, 79 | uint256 stake, 80 | uint256 duration, 81 | string url, 82 | address creator 83 | ); 84 | event ScholarshipCreated( 85 | uint256 indexed courseId, 86 | uint256 scholarshipAmount, 87 | uint256 newScholars, 88 | uint256 scholarshipTotal, 89 | address scholarshipProvider, 90 | address scholarshipVault, 91 | uint256 scholarshipYield 92 | ); 93 | event ScholarRegistered( 94 | uint256 indexed courseId, 95 | address scholar 96 | ); 97 | event ScholarshipWithdrawn( 98 | uint256 indexed courseId, 99 | uint256 amountWithdrawn 100 | ); 101 | event LearnerRegistered( 102 | uint256 indexed courseId, 103 | address learner 104 | ); 105 | event StakeRedeemed( 106 | uint256 courseId, 107 | address learner, 108 | uint256 amount 109 | ); 110 | event LearnMintedFromCourse( 111 | uint256 courseId, 112 | address learner, 113 | uint256 stableConverted, 114 | uint256 learnMinted 115 | ); 116 | event BatchDeposited( 117 | uint256 batchId, 118 | uint256 batchAmount, 119 | uint256 batchYieldAmount 120 | ); 121 | event YieldRewardRedeemed( 122 | address redeemer, 123 | uint256 yieldRewarded 124 | ); 125 | 126 | constructor( 127 | address _stable, 128 | address _learningCurve, 129 | address _registry 130 | ) { 131 | stable = ERC20(_stable); 132 | learningCurve = I_LearningCurve(_learningCurve); 133 | registry = I_Registry(_registry); 134 | } 135 | 136 | /** 137 | * @notice create a course 138 | * @param _stake stake required to register 139 | * @param _duration the duration of the course, in number of blocks 140 | * @param _url url leading to course details 141 | * @param _creator the address that excess yield will be sent to on a redeem 142 | */ 143 | function createCourse( 144 | uint256 _stake, 145 | uint256 _duration, 146 | string calldata _url, 147 | address _creator 148 | ) external { 149 | require( 150 | _stake > 0, 151 | "createCourse: stake must be greater than 0" 152 | ); 153 | require( 154 | _duration > 0, 155 | "createCourse: duration must be greater than 0" 156 | ); 157 | require( 158 | _creator != address(0), 159 | "createCourse: creator cannot be 0 address" 160 | ); 161 | uint256 courseId_ = courseIdTracker; 162 | courseIdTracker++; 163 | courses[courseId_] = Course( 164 | _stake, 165 | _duration, 166 | _url, 167 | _creator, 168 | 0, // no scholars when a course is first created 169 | 0, // scholarshipAmount is similarly 0. 170 | 0, // no completed scholars yet 171 | address(0), // scholarshipVault address unset at course creation 172 | 0 // scholarshipYield also 0 173 | ); 174 | emit CourseCreated( 175 | courseId_, 176 | _stake, 177 | _duration, 178 | _url, 179 | _creator 180 | ); 181 | } 182 | 183 | /** 184 | * @notice this method allows anyone to create perpetual scholarships by staking 185 | * capital for learners to use. The can claim it back at any time. 186 | * @param _courseId course id the donor would like to fund 187 | * @param _amount the amount in DAI that the donor wishes to give 188 | */ 189 | function createScholarships(uint256 _courseId, uint256 _amount) 190 | public 191 | { 192 | require( 193 | _courseId < courseIdTracker, 194 | "createScholarships: courseId does not exist" 195 | ); 196 | require( 197 | _amount >= courses[_courseId].stake, 198 | "createScholarships: must seed scholarship with enough funds to justify gas costs" 199 | ); 200 | Course storage course = courses[_courseId]; 201 | 202 | SafeTransferLib.safeTransferFrom(stable, msg.sender, address(this), _amount); 203 | 204 | // get the address of the scholarshipVault if it exists, otherwise get the latest vault from the yRegistry 205 | if (course.scholarshipVault != address(0)) { 206 | I_Vault vault = I_Vault(course.scholarshipVault); 207 | stable.approve(course.scholarshipVault, _amount); 208 | course.scholarshipYTokens += vault.deposit(_amount); 209 | } else { 210 | I_Vault newVault = I_Vault(registry.latestVault(address(stable))); 211 | course.scholarshipVault = address(newVault); 212 | stable.approve(course.scholarshipVault, _amount); 213 | course.scholarshipYTokens = newVault.deposit(_amount); 214 | } 215 | 216 | // set providerData to ensure withdrawals are possible 217 | providerAmount[_courseId][msg.sender] += _amount; 218 | 219 | // add this scholarship provided to any pre-existing amount 220 | course.scholarshipTotal += _amount; 221 | 222 | emit ScholarshipCreated( 223 | _courseId, 224 | _amount, 225 | _amount / course.stake, // amount scholars this specific scholarship creates 226 | course.scholarshipTotal, 227 | msg.sender, 228 | course.scholarshipVault, 229 | course.scholarshipYTokens 230 | ); 231 | } 232 | 233 | /** 234 | * @notice handles learner registration with permit. This enable learners to register with only one transaction, 235 | * rather than two, i.e. approve DeSchool to spend your DAI, and only then register. This saves gas for 236 | * learners and improves the UX. 237 | * @param _courseId course id for which the learner wishes to register 238 | * @param nonce provided in the 2616 standard for replay protection. 239 | * @param expiry the current blocktime must be less than or equal to this for a valid transaction 240 | * @param v a recovery identity variable included in Ethereum, in addition to the r and s below which are standard ECDSA parameters 241 | * @param r standard ECDSA parameter 242 | * @param s standard ECDSA parameter 243 | */ 244 | function permitCreateScholarships( 245 | uint256 _courseId, 246 | uint256 _amount, 247 | uint256 nonce, 248 | uint256 expiry, 249 | uint8 v, 250 | bytes32 r, 251 | bytes32 s 252 | ) external { 253 | IERC20Permit(address(stable)).permit(msg.sender, address(this), nonce, expiry, true, v, r, s); 254 | createScholarships(_courseId, _amount); 255 | } 256 | 257 | /** 258 | * @notice handles scholar registration if there are scholarship available 259 | * @param _courseId course id the scholar would like to register to 260 | */ 261 | function registerScholar(uint256 _courseId) 262 | public 263 | { 264 | require( 265 | _courseId < courseIdTracker, 266 | "registerScholar: courseId does not exist" 267 | ); 268 | Course storage course = courses[_courseId]; 269 | if (registered[_courseId][msg.sender].blockRegistered != 0) { 270 | revert("registerScholar: already registered"); 271 | } 272 | // Perpetual scholarships are enabled on an as needed basis - it is most gas efficient 273 | if ((course.scholarshipTotal / course.stake) <= course.scholars) { 274 | if (scholarData[_courseId][course.completedScholars].blockRegistered + course.duration <= block.number) { 275 | scholarData[_courseId][course.scholars].blockRegistered = block.number; 276 | registered[_courseId][msg.sender].blockRegistered = block.number; 277 | course.completedScholars++; 278 | course.scholars++; 279 | } else { 280 | revert("registerScholar: no scholarships available for this course"); 281 | } 282 | } else { 283 | scholarData[_courseId][course.scholars].blockRegistered = block.number; 284 | registered[_courseId][msg.sender].blockRegistered = block.number; 285 | course.scholars++; 286 | } 287 | 288 | emit ScholarRegistered( 289 | _courseId, 290 | msg.sender 291 | ); 292 | } 293 | 294 | /** 295 | * @notice allows donor to withdraw their scholarship donation, or a portion thereof, at any point 296 | * Q: what happens if there are still learners registered for the course and the scholarship is withdrawn from under them? 297 | * A: allow them to complete the course, but allow no new scholars after withdraw takes place. 298 | * @param _courseId course id from which the scholarship is to be withdrawn. 299 | * @param _amount the amount that the scholarship provider wishes to withdraw 300 | */ 301 | function withdrawScholarship(uint256 _courseId, uint256 _amount) 302 | public 303 | { 304 | require( 305 | providerAmount[_courseId][msg.sender] >= _amount, 306 | "withdrawScholarship: can only withdraw up to the amount initally provided for scholarships" 307 | ); 308 | Course storage course = courses[_courseId]; 309 | I_Vault vault = I_Vault(course.scholarshipVault); 310 | // get the proportional amount of shares the user owns 311 | uint256 providerShares = (((_amount * 1e18) / course.scholarshipTotal) * (course.scholarshipYTokens)) / 1e18; 312 | // in case of rounding errors we want to cap provider shares at the total number of ytokens on the scholarship 313 | if (providerShares > course.scholarshipYTokens) { 314 | providerShares = course.scholarshipYTokens; 315 | } 316 | // reduce the number of ytokens 317 | course.scholarshipYTokens -= providerShares; 318 | // first, mark down the amount provided 319 | providerAmount[_courseId][msg.sender] -= _amount; 320 | // we only need to subtract from the total scholarship for this course, as that is what is used to 321 | // check when registering new scholars. 322 | course.scholarshipTotal -= _amount; 323 | 324 | emit ScholarshipWithdrawn( 325 | _courseId, 326 | _amount 327 | ); 328 | // withdraw amount from scholarshipVault for this course and return to provider 329 | uint256 collateral = vault.withdraw(providerShares); 330 | // check if the collateral returned is greater than the amount passed in 331 | // if it is then we take away the excess and allocate it to the course creator 332 | // as yield rewards and send the original amount back to the scholarship provider 333 | if (collateral > _amount) { 334 | yieldRewards[course.creator] = collateral - _amount; 335 | collateral = _amount; 336 | } 337 | SafeTransferLib.safeTransfer(stable, msg.sender, collateral); 338 | } 339 | 340 | /** 341 | * @notice deposit the current batch of DAI in the contract to yearn. 342 | * the batching mechanism is used to reduce gas for each learner, 343 | * so at any point someone can call this function and deploy all 344 | * funds in a specific "batch" to yearn, allowing the funds to gain 345 | * interest. 346 | */ 347 | function batchDeposit() 348 | external 349 | { 350 | uint256 batchId_ = batchIdTracker; 351 | // initiate the next batch 352 | uint256 batchAmount_ = batchTotal[batchId_]; 353 | batchIdTracker++; 354 | require(batchAmount_ > 0, "batchDeposit: no funds to deposit"); 355 | // get the address of the vault from the yRegistry 356 | I_Vault vault = I_Vault(registry.latestVault(address(stable))); 357 | // approve the vault 358 | stable.approve(address(vault), batchAmount_); 359 | // mint y from the vault 360 | uint256 yTokens = vault.deposit(batchAmount_); 361 | batchYieldTotal[batchId_] = yTokens; 362 | batchYieldAddress[batchId_] = address(vault); 363 | emit BatchDeposited(batchId_, batchAmount_, yTokens); 364 | } 365 | 366 | /** 367 | * @notice handles learner registration in the case that no scholarships are available 368 | * @param _courseId course id the learner would like to register to 369 | */ 370 | function register(uint256 _courseId) 371 | public 372 | { 373 | require( 374 | _courseId < courseIdTracker, 375 | "register: courseId does not exist" 376 | ); 377 | uint256 batchId_ = batchIdTracker; 378 | require( 379 | learnerData[_courseId][msg.sender].blockRegistered == 0, 380 | "register: already registered" 381 | ); 382 | Course memory course = courses[_courseId]; 383 | 384 | SafeTransferLib.safeTransferFrom(stable, msg.sender, address(this), course.stake); 385 | 386 | learnerData[_courseId][msg.sender].blockRegistered = block.number; 387 | learnerData[_courseId][msg.sender].yieldBatchId = batchId_; 388 | batchTotal[batchId_] += course.stake; 389 | 390 | emit LearnerRegistered( 391 | _courseId, 392 | msg.sender 393 | ); 394 | } 395 | 396 | /** 397 | * @notice handles learner registration with permit. This enable learners to register with only one transaction, 398 | * rather than two, i.e. approve DeSchool to spend your DAI, and only then register. This saves gas for 399 | * learners and improves the UX. 400 | * @param _courseId course id for which the learner wishes to register 401 | * @param nonce provided in the 2616 standard for replay protection. 402 | * @param expiry the current blocktime must be less than or equal to this for a valid transaction 403 | * @param v a recovery identity variable included in Ethereum, in addition to the r and s below which are standard ECDSA parameters 404 | * @param r standard ECDSA parameter 405 | * @param s standard ECDSA parameter 406 | */ 407 | function permitAndRegister( 408 | uint256 _courseId, 409 | uint256 nonce, 410 | uint256 expiry, 411 | uint8 v, 412 | bytes32 r, 413 | bytes32 s 414 | ) external { 415 | IERC20Permit(address(stable)).permit(msg.sender, address(this), nonce, expiry, true, v, r, s); 416 | register(_courseId); 417 | } 418 | 419 | /** 420 | * @notice All courses are deployed with a duration in blocks, after which learners 421 | * can either claim their stake back, or use it to mint LEARN 422 | * @param _learner address of the learner to verify 423 | * @param _courseId course id to verify for the learner 424 | * @return completed if the full course duration has passed and the learner can redeem their stake or mint LEARN 425 | */ 426 | function verify(address _learner, uint256 _courseId) 427 | public 428 | view 429 | returns (bool completed) 430 | { 431 | require( 432 | _courseId < courseIdTracker, 433 | "verify: courseId does not exist" 434 | ); 435 | require( 436 | learnerData[_courseId][_learner].blockRegistered != 0, 437 | "verify: not registered to this course" 438 | ); 439 | if (courses[_courseId].duration < block.number - learnerData[_courseId][_learner].blockRegistered) { 440 | return true; 441 | } 442 | } 443 | 444 | /** 445 | * @notice handles stake redemption in DAI 446 | * if a learner is redeeming rather than minting, it means 447 | * they are simply requesting their initial stake back. 448 | * In this case, we check that the course duration has passed and, 449 | * if so, send the full stake back to the learner. 450 | * 451 | * Whatever yield was earned is sent to the course creator address. 452 | * 453 | * @param _courseId course id to redeem the stake from 454 | */ 455 | function redeem(uint256 _courseId) 456 | external 457 | { 458 | uint256 collateral; 459 | uint256 learnerShares; 460 | require( 461 | learnerData[_courseId][msg.sender].blockRegistered != 0, 462 | "redeem: not a learner on this course" 463 | ); 464 | require( 465 | verify(msg.sender, _courseId), 466 | "redeem: not yet eligible - wait for the full course duration to pass" 467 | ); 468 | Course memory course = courses[_courseId]; 469 | if (isDeployed(_courseId)) { 470 | I_Vault vault = I_Vault( 471 | batchYieldAddress[ 472 | learnerData[_courseId][msg.sender].yieldBatchId 473 | ] 474 | ); 475 | uint256 batchId_ = learnerData[_courseId][msg.sender].yieldBatchId; 476 | uint256 temp = (course.stake * 1e18) / batchTotal[batchId_]; 477 | learnerShares = (temp * batchYieldTotal[batchId_]) / 1e18; 478 | collateral = vault.withdraw(learnerShares); 479 | if (course.stake < collateral) { 480 | yieldRewards[course.creator] += collateral - course.stake; 481 | emit StakeRedeemed(_courseId, msg.sender, course.stake); 482 | SafeTransferLib.safeTransfer(stable, msg.sender, course.stake); 483 | } else { 484 | emit StakeRedeemed( 485 | _courseId, 486 | msg.sender, 487 | collateral 488 | ); 489 | SafeTransferLib.safeTransfer(stable, msg.sender, collateral); 490 | } 491 | } else { 492 | emit StakeRedeemed( 493 | _courseId, 494 | msg.sender, 495 | course.stake 496 | ); 497 | SafeTransferLib.safeTransfer(stable, msg.sender, course.stake); 498 | } 499 | } 500 | 501 | /** 502 | * @notice handles learner minting new LEARN 503 | * checks via verify() that the original stake can be redeemed and used 504 | * to mint via the Learning Curve. 505 | * Any yield earned on the original stake is sent to 506 | * the creator's designated address. 507 | * All the resulting LEARN tokens are returned to the learner. 508 | * @param _courseId course id to mint LEARN from 509 | */ 510 | function mint(uint256 _courseId) 511 | external 512 | { 513 | uint256 collateral; 514 | uint256 learnerShares; 515 | require( 516 | learnerData[_courseId][msg.sender].blockRegistered != 0, 517 | "mint: not a learner on this course" 518 | ); 519 | require( 520 | verify(msg.sender, _courseId), 521 | "mint: not yet eligible - wait for the full course duration to pass" 522 | ); 523 | Course memory course = courses[_courseId]; 524 | if (isDeployed(_courseId)) { 525 | I_Vault vault = I_Vault( 526 | batchYieldAddress[ 527 | learnerData[_courseId][msg.sender].yieldBatchId 528 | ] 529 | ); 530 | uint256 batchId_ = learnerData[_courseId][msg.sender].yieldBatchId; 531 | uint256 temp = (course.stake * 1e18) / batchTotal[batchId_]; 532 | learnerShares = (temp * batchYieldTotal[batchId_]) / 1e18; 533 | collateral = vault.withdraw(learnerShares); 534 | } 535 | if (course.stake < collateral) { 536 | yieldRewards[course.creator] += collateral - course.stake; 537 | stable.approve(address(learningCurve), course.stake); 538 | uint256 balanceBefore = learningCurve.balanceOf(msg.sender); 539 | learningCurve.mintForAddress(msg.sender, course.stake); 540 | emit LearnMintedFromCourse( 541 | _courseId, 542 | msg.sender, 543 | course.stake, 544 | learningCurve.balanceOf(msg.sender) - balanceBefore 545 | ); 546 | } else { 547 | stable.approve(address(learningCurve), course.stake); 548 | uint256 balanceBefore = learningCurve.balanceOf(msg.sender); 549 | learningCurve.mintForAddress(msg.sender, course.stake); 550 | emit LearnMintedFromCourse( 551 | _courseId, 552 | msg.sender, 553 | course.stake, 554 | learningCurve.balanceOf(msg.sender) - balanceBefore 555 | ); 556 | } 557 | } 558 | 559 | /** 560 | * @notice Gets the yield a creator can claim, which comes from two sources. 561 | * There may be yield from scholarships provided for their course, which is assigned as 562 | * the scholarship is created and may be claimed at any time thereafter. 563 | * There may also be yield from any learners who have registered in the case no scholarships are available. 564 | * When the learner decides to redeem or mint their stake, this yield is assigned to the creator. 565 | */ 566 | function withdrawYieldRewards() 567 | external 568 | { 569 | uint256 withdrawableReward; 570 | // add to the withdrawableRewards any yield from learner deposits who are not scholars 571 | withdrawableReward = yieldRewards[msg.sender]; 572 | require(withdrawableReward > 0, "withdrawYieldRewards: No yield to withdraw"); 573 | yieldRewards[msg.sender] = 0; 574 | emit YieldRewardRedeemed(msg.sender, withdrawableReward); 575 | SafeTransferLib.safeTransfer(stable, msg.sender, withdrawableReward); 576 | } 577 | 578 | /** 579 | * @notice check whether a learner's staked has been deployed to a Yearn vault 580 | * @param _courseId course id to redeem stake or mint LEARN from 581 | * @return deployed whether the funds to be redeemed were deployed to yearn 582 | */ 583 | function isDeployed(uint256 _courseId) 584 | internal 585 | view 586 | returns (bool deployed) 587 | { 588 | uint256 batchId_ = learnerData[_courseId][msg.sender].yieldBatchId; 589 | if (batchId_ == batchIdTracker) { 590 | return false; 591 | } else { 592 | return true; 593 | } 594 | } 595 | 596 | function scholarshipAvailable(uint256 _courseId) 597 | external 598 | view 599 | returns (bool) 600 | { 601 | Course memory course = courses[_courseId]; 602 | return (course.scholarshipTotal / course.stake) > course.scholars || 603 | scholarData[_courseId][course.completedScholars].blockRegistered + course.duration <= block.number; 604 | } 605 | 606 | function getCurrentBatchTotal() 607 | external 608 | view 609 | returns (uint256) 610 | { 611 | return batchTotal[batchIdTracker]; 612 | } 613 | 614 | function getBlockRegistered(address learner, uint256 courseId) 615 | external 616 | view 617 | returns (uint256) 618 | { 619 | return learnerData[courseId][learner].blockRegistered; 620 | } 621 | 622 | function getCurrentBatchId() 623 | external 624 | view 625 | returns (uint256) 626 | { 627 | return batchIdTracker; 628 | } 629 | 630 | function getNextCourseId() 631 | external 632 | view 633 | returns (uint256) 634 | { 635 | return courseIdTracker; 636 | } 637 | 638 | function getCourseUrl(uint256 _courseId) 639 | external 640 | view 641 | returns (string memory) 642 | { 643 | return courses[_courseId].url; 644 | } 645 | 646 | function getYieldRewards(address creator) external view returns (uint256) { 647 | return yieldRewards[creator]; 648 | } 649 | } 650 | -------------------------------------------------------------------------------- /contracts/ERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | pragma solidity >=0.8.0; 3 | 4 | /// @notice Modern and gas efficient ERC20 + EIP-2612 implementation. 5 | /// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/tokens/ERC20.sol) 6 | /// @author Modified from Uniswap (https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/UniswapV2ERC20.sol) 7 | /// @dev Do not manually set balances without updating totalSupply, as the sum of all user balances must not exceed it. 8 | abstract contract ERC20 { 9 | /*/////////////////////////////////////////////////////////////// 10 | EVENTS 11 | //////////////////////////////////////////////////////////////*/ 12 | 13 | event Transfer(address indexed from, address indexed to, uint256 amount); 14 | 15 | event Approval(address indexed owner, address indexed spender, uint256 amount); 16 | 17 | /*/////////////////////////////////////////////////////////////// 18 | METADATA STORAGE 19 | //////////////////////////////////////////////////////////////*/ 20 | 21 | string public name; 22 | 23 | string public symbol; 24 | 25 | uint8 public immutable decimals; 26 | 27 | /*/////////////////////////////////////////////////////////////// 28 | ERC20 STORAGE 29 | //////////////////////////////////////////////////////////////*/ 30 | 31 | uint256 public totalSupply; 32 | 33 | mapping(address => uint256) public balanceOf; 34 | 35 | mapping(address => mapping(address => uint256)) public allowance; 36 | 37 | /*/////////////////////////////////////////////////////////////// 38 | EIP-2612 STORAGE 39 | //////////////////////////////////////////////////////////////*/ 40 | 41 | uint256 internal immutable INITIAL_CHAIN_ID; 42 | 43 | bytes32 internal immutable INITIAL_DOMAIN_SEPARATOR; 44 | 45 | mapping(address => uint256) public nonces; 46 | 47 | /*/////////////////////////////////////////////////////////////// 48 | CONSTRUCTOR 49 | //////////////////////////////////////////////////////////////*/ 50 | 51 | constructor( 52 | string memory _name, 53 | string memory _symbol, 54 | uint8 _decimals 55 | ) { 56 | name = _name; 57 | symbol = _symbol; 58 | decimals = _decimals; 59 | 60 | INITIAL_CHAIN_ID = block.chainid; 61 | INITIAL_DOMAIN_SEPARATOR = computeDomainSeparator(); 62 | } 63 | 64 | /*/////////////////////////////////////////////////////////////// 65 | ERC20 LOGIC 66 | //////////////////////////////////////////////////////////////*/ 67 | 68 | function approve(address spender, uint256 amount) public virtual returns (bool) { 69 | allowance[msg.sender][spender] = amount; 70 | 71 | emit Approval(msg.sender, spender, amount); 72 | 73 | return true; 74 | } 75 | 76 | function transfer(address to, uint256 amount) public virtual returns (bool) { 77 | balanceOf[msg.sender] -= amount; 78 | 79 | // Cannot overflow because the sum of all user 80 | // balances can't exceed the max uint256 value. 81 | unchecked { 82 | balanceOf[to] += amount; 83 | } 84 | 85 | emit Transfer(msg.sender, to, amount); 86 | 87 | return true; 88 | } 89 | 90 | function transferFrom( 91 | address from, 92 | address to, 93 | uint256 amount 94 | ) public virtual returns (bool) { 95 | uint256 allowed = allowance[from][msg.sender]; // Saves gas for limited approvals. 96 | 97 | if (allowed != type(uint256).max) allowance[from][msg.sender] = allowed - amount; 98 | 99 | balanceOf[from] -= amount; 100 | 101 | // Cannot overflow because the sum of all user 102 | // balances can't exceed the max uint256 value. 103 | unchecked { 104 | balanceOf[to] += amount; 105 | } 106 | 107 | emit Transfer(from, to, amount); 108 | 109 | return true; 110 | } 111 | 112 | /*/////////////////////////////////////////////////////////////// 113 | EIP-2612 LOGIC 114 | //////////////////////////////////////////////////////////////*/ 115 | 116 | function permit( 117 | address owner, 118 | address spender, 119 | uint256 value, 120 | uint256 deadline, 121 | uint8 v, 122 | bytes32 r, 123 | bytes32 s 124 | ) public virtual { 125 | require(deadline >= block.timestamp, "PERMIT_DEADLINE_EXPIRED"); 126 | 127 | // Unchecked because the only math done is incrementing 128 | // the owner's nonce which cannot realistically overflow. 129 | unchecked { 130 | bytes32 digest = keccak256( 131 | abi.encodePacked( 132 | "\x19\x01", 133 | DOMAIN_SEPARATOR(), 134 | keccak256( 135 | abi.encode( 136 | keccak256( 137 | "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" 138 | ), 139 | owner, 140 | spender, 141 | value, 142 | nonces[owner]++, 143 | deadline 144 | ) 145 | ) 146 | ) 147 | ); 148 | 149 | address recoveredAddress = ecrecover(digest, v, r, s); 150 | 151 | require(recoveredAddress != address(0) && recoveredAddress == owner, "INVALID_SIGNER"); 152 | 153 | allowance[recoveredAddress][spender] = value; 154 | } 155 | 156 | emit Approval(owner, spender, value); 157 | } 158 | 159 | function DOMAIN_SEPARATOR() public view virtual returns (bytes32) { 160 | return block.chainid == INITIAL_CHAIN_ID ? INITIAL_DOMAIN_SEPARATOR : computeDomainSeparator(); 161 | } 162 | 163 | function computeDomainSeparator() internal view virtual returns (bytes32) { 164 | return 165 | keccak256( 166 | abi.encode( 167 | keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), 168 | keccak256(bytes(name)), 169 | keccak256("1"), 170 | block.chainid, 171 | address(this) 172 | ) 173 | ); 174 | } 175 | 176 | /*/////////////////////////////////////////////////////////////// 177 | INTERNAL MINT/BURN LOGIC 178 | //////////////////////////////////////////////////////////////*/ 179 | 180 | function _mint(address to, uint256 amount) internal virtual { 181 | totalSupply += amount; 182 | 183 | // Cannot overflow because the sum of all user 184 | // balances can't exceed the max uint256 value. 185 | unchecked { 186 | balanceOf[to] += amount; 187 | } 188 | 189 | emit Transfer(address(0), to, amount); 190 | } 191 | 192 | function _burn(address from, uint256 amount) internal virtual { 193 | balanceOf[from] -= amount; 194 | 195 | // Cannot underflow because a user's balance 196 | // will never be larger than the total supply. 197 | unchecked { 198 | totalSupply -= amount; 199 | } 200 | 201 | emit Transfer(from, address(0), amount); 202 | } 203 | } -------------------------------------------------------------------------------- /contracts/LearningCurve.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MPL-2.0 2 | pragma solidity 0.8.13; 3 | 4 | import "./ERC20.sol"; 5 | import "./PRBMath.sol"; 6 | import "./PRBMathUD60x18.sol"; 7 | import "./SafeTransferLib.sol"; 8 | import "./interfaces/IERC20Permit.sol"; 9 | 10 | /** 11 | * @title LearningCurve 12 | * @notice A simple constant product curve that mints LEARN tokens whenever 13 | * anyone sends it DAI, or burns LEARN tokens and returns DAI. 14 | */ 15 | contract LearningCurve is ERC20 { 16 | 17 | // the constant product used in the curve 18 | uint256 public constant k = 10000; 19 | ERC20 public reserve; 20 | uint256 public reserveBalance; 21 | bool initialised; 22 | 23 | event LearnMinted( 24 | address indexed learner, 25 | uint256 amountMinted, 26 | uint256 daiDeposited 27 | ); 28 | event LearnBurned( 29 | address indexed learner, 30 | uint256 amountBurned, 31 | uint256 daiReturned, 32 | uint256 e 33 | ); 34 | 35 | constructor(address _reserve) ERC20("FreeLearn", "LEARN", 18) { 36 | reserve = ERC20(_reserve); 37 | } 38 | 39 | /** 40 | * @notice initialise the contract, mainly for maths purposes, requires the transfer of 1 DAI. 41 | * @dev only callable once 42 | */ 43 | function initialise() external { 44 | require(!initialised, "initialised"); 45 | initialised = true; 46 | SafeTransferLib.safeTransferFrom(reserve, msg.sender, address(this), 1e18); 47 | reserveBalance += 1e18; 48 | _mint(address(this), 10001e18); 49 | } 50 | 51 | /** 52 | * @notice handles LEARN mint with an approval for DAI 53 | */ 54 | function permitAndMint(uint256 _amount, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s) external { 55 | IERC20Permit(address(reserve)).permit(msg.sender, address(this), nonce, expiry, true, v, r, s); 56 | mint(_amount); 57 | } 58 | /** 59 | * @notice This method allows anyone to mint LEARN tokens dependent on the 60 | * amount of DAI they send. 61 | * 62 | * The amount minted depends on the amount of collateral already locked in 63 | * the curve. The more DAI is locked, the less LEARN gets minted, ensuring 64 | * that the price of LEARN increases linearly. 65 | * 66 | * Please see: https://docs.google.com/spreadsheets/d/1hjWFGPC_B9D7b6iI00DTVVLrqRFv3G5zFNiCBS7y_V8/edit?usp=sharing 67 | * @param _wad amount of Dai to send to the contract 68 | */ 69 | function mint(uint256 _wad) public { 70 | require(initialised, "!initialised"); 71 | SafeTransferLib.safeTransferFrom(reserve, msg.sender, address(this), _wad); 72 | uint256 ln = doLn((((reserveBalance + _wad) * 1e18)) / reserveBalance); 73 | uint256 learnMagic = k * ln; 74 | reserveBalance += _wad; 75 | _mint(msg.sender, learnMagic); 76 | emit LearnMinted(msg.sender, learnMagic, _wad); 77 | } 78 | 79 | /** 80 | * @notice Same as normal mint, except that an address is passed in which the minted 81 | * LEARN is sent to. Necessary to allow for mints directly from a Course, where 82 | * we want to learner to receive LEARN, not the course contract. 83 | * 84 | * Can be used to send DAI from one address and have LEARN returned to another. 85 | * @param learner address of the learner to mint LEARN to 86 | * @param _wad amount of DAI being sent in. 87 | */ 88 | function mintForAddress(address learner, uint256 _wad) public { 89 | require(initialised, "!initialised"); 90 | SafeTransferLib.safeTransferFrom(reserve, msg.sender, address(this), _wad); 91 | uint256 ln = doLn((((reserveBalance + _wad) * 1e18)) / reserveBalance); 92 | uint256 learnMagic = k * ln; 93 | reserveBalance += _wad; 94 | _mint(learner, learnMagic); 95 | emit LearnMinted(learner, learnMagic, _wad); 96 | } 97 | 98 | /** 99 | * @notice used to burn LEARN and return DAI to the sender. 100 | * @param _burnAmount amount of LEARN to burn 101 | */ 102 | function burn(uint256 _burnAmount) public { 103 | require(initialised, "!initialised"); 104 | uint256 e = e_calc(_burnAmount); 105 | uint256 learnMagic = reserveBalance - (reserveBalance * 1e18) / e; 106 | _burn(msg.sender, _burnAmount); 107 | reserveBalance -= learnMagic; 108 | SafeTransferLib.safeTransfer(reserve, msg.sender, learnMagic); 109 | emit LearnBurned(msg.sender, _burnAmount, learnMagic, e); 110 | } 111 | 112 | /** 113 | * @notice Calculates the natural exponent of the inputted value 114 | * @param x the number to be used in the natural log calc 115 | */ 116 | function e_calc(uint256 x) internal pure returns (uint256 result) { 117 | PRBMath.UD60x18 memory xud = PRBMath.UD60x18({value: x / k}); 118 | result = PRBMathUD60x18.exp(xud).value; 119 | } 120 | 121 | /** 122 | * @notice Calculates the natural logarithm of x. 123 | * @param x the number to be used in the natural log calc 124 | * @return result the natural log of the inputted value 125 | */ 126 | function doLn(uint256 x) internal pure returns (uint256 result) { 127 | PRBMath.UD60x18 memory xud = PRBMath.UD60x18({value: x}); 128 | result = PRBMathUD60x18.ln(xud).value; 129 | } 130 | 131 | /** 132 | * @notice calculates the amount of reserve received for a burn amount 133 | * @param _burnAmount the amount of LEARN to burn 134 | * @return learnMagic the dai receivable for a certain amount of burnt LEARN 135 | */ 136 | function getPredictedBurn(uint256 _burnAmount) 137 | external 138 | view 139 | returns (uint256 learnMagic) 140 | { 141 | uint256 e = e_calc(_burnAmount); 142 | learnMagic = reserveBalance - (reserveBalance * 1e18) / e; 143 | } 144 | 145 | /** 146 | * @notice calculates the amount of LEARN to mint given the amount of DAI requested. 147 | * @param reserveAmount the amount of DAI to lock 148 | * @return learnMagic the LEARN mintable for a certain amount of dai 149 | */ 150 | function getMintableForReserveAmount(uint256 reserveAmount) 151 | external 152 | view 153 | returns (uint256 learnMagic) 154 | { 155 | uint256 ln = doLn( 156 | (((reserveBalance + reserveAmount) * 1e18)) / reserveBalance 157 | ); 158 | learnMagic = k * ln; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /contracts/PRBMath.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: WTFPL 2 | pragma solidity >=0.8.0; 3 | 4 | /// @dev Common mathematical functions used in both PRBMathSD59x18 and PRBMathUD60x18. Note that this shared library 5 | /// does not always assume the signed 59.18-decimal fixed-point or the unsigned 60.18-decimal fixed-point 6 | // representation. When it does not, it is annonated in the function's NatSpec documentation. 7 | library PRBMath { 8 | /// STRUCTS /// 9 | 10 | struct SD59x18 { 11 | int256 value; 12 | } 13 | 14 | struct UD60x18 { 15 | uint256 value; 16 | } 17 | 18 | /// STORAGE /// 19 | 20 | /// @dev How many trailing decimals can be represented. 21 | uint256 internal constant SCALE = 1e18; 22 | 23 | /// @dev Largest power of two divisor of SCALE. 24 | uint256 internal constant SCALE_LPOTD = 262144; 25 | 26 | /// @dev SCALE inverted mod 2^256. 27 | uint256 internal constant SCALE_INVERSE = 28 | 78156646155174841979727994598816262306175212592076161876661508869554232690281; 29 | 30 | /// @notice Calculates the binary exponent of x using the binary fraction method. 31 | /// @dev Uses 128.128-bit fixed-point numbers, which is the most efficient way. 32 | /// See https://ethereum.stackexchange.com/a/96594/24693. 33 | /// @param x The exponent as an unsigned 128.128-bit fixed-point number. 34 | /// @return result The result as an unsigned 60x18 decimal fixed-point number. 35 | function exp2(uint256 x) internal pure returns (uint256 result) { 36 | unchecked { 37 | // Start from 0.5 in the 128.128-bit fixed-point format. 38 | result = 0x80000000000000000000000000000000; 39 | 40 | // Multiply the result by root(2, 2^-i) when the bit at position i is 1. None of the intermediary results overflows 41 | // because the initial result is 2^127 and all magic factors are less than 2^129. 42 | if (x & 0x80000000000000000000000000000000 > 0) 43 | result = (result * 0x16A09E667F3BCC908B2FB1366EA957D3E) >> 128; 44 | if (x & 0x40000000000000000000000000000000 > 0) 45 | result = (result * 0x1306FE0A31B7152DE8D5A46305C85EDED) >> 128; 46 | if (x & 0x20000000000000000000000000000000 > 0) 47 | result = (result * 0x1172B83C7D517ADCDF7C8C50EB14A7920) >> 128; 48 | if (x & 0x10000000000000000000000000000000 > 0) 49 | result = (result * 0x10B5586CF9890F6298B92B71842A98364) >> 128; 50 | if (x & 0x8000000000000000000000000000000 > 0) 51 | result = (result * 0x1059B0D31585743AE7C548EB68CA417FE) >> 128; 52 | if (x & 0x4000000000000000000000000000000 > 0) 53 | result = (result * 0x102C9A3E778060EE6F7CACA4F7A29BDE9) >> 128; 54 | if (x & 0x2000000000000000000000000000000 > 0) 55 | result = (result * 0x10163DA9FB33356D84A66AE336DCDFA40) >> 128; 56 | if (x & 0x1000000000000000000000000000000 > 0) 57 | result = (result * 0x100B1AFA5ABCBED6129AB13EC11DC9544) >> 128; 58 | if (x & 0x800000000000000000000000000000 > 0) 59 | result = (result * 0x10058C86DA1C09EA1FF19D294CF2F679C) >> 128; 60 | if (x & 0x400000000000000000000000000000 > 0) 61 | result = (result * 0x1002C605E2E8CEC506D21BFC89A23A011) >> 128; 62 | if (x & 0x200000000000000000000000000000 > 0) 63 | result = (result * 0x100162F3904051FA128BCA9C55C31E5E0) >> 128; 64 | if (x & 0x100000000000000000000000000000 > 0) 65 | result = (result * 0x1000B175EFFDC76BA38E31671CA939726) >> 128; 66 | if (x & 0x80000000000000000000000000000 > 0) 67 | result = (result * 0x100058BA01FB9F96D6CACD4B180917C3E) >> 128; 68 | if (x & 0x40000000000000000000000000000 > 0) 69 | result = (result * 0x10002C5CC37DA9491D0985C348C68E7B4) >> 128; 70 | if (x & 0x20000000000000000000000000000 > 0) 71 | result = (result * 0x1000162E525EE054754457D5995292027) >> 128; 72 | if (x & 0x10000000000000000000000000000 > 0) 73 | result = (result * 0x10000B17255775C040618BF4A4ADE83FD) >> 128; 74 | if (x & 0x8000000000000000000000000000 > 0) 75 | result = (result * 0x1000058B91B5BC9AE2EED81E9B7D4CFAC) >> 128; 76 | if (x & 0x4000000000000000000000000000 > 0) 77 | result = (result * 0x100002C5C89D5EC6CA4D7C8ACC017B7CA) >> 128; 78 | if (x & 0x2000000000000000000000000000 > 0) 79 | result = (result * 0x10000162E43F4F831060E02D839A9D16D) >> 128; 80 | if (x & 0x1000000000000000000000000000 > 0) 81 | result = (result * 0x100000B1721BCFC99D9F890EA06911763) >> 128; 82 | if (x & 0x800000000000000000000000000 > 0) 83 | result = (result * 0x10000058B90CF1E6D97F9CA14DBCC1629) >> 128; 84 | if (x & 0x400000000000000000000000000 > 0) 85 | result = (result * 0x1000002C5C863B73F016468F6BAC5CA2C) >> 128; 86 | if (x & 0x200000000000000000000000000 > 0) 87 | result = (result * 0x100000162E430E5A18F6119E3C02282A6) >> 128; 88 | if (x & 0x100000000000000000000000000 > 0) 89 | result = (result * 0x1000000B1721835514B86E6D96EFD1BFF) >> 128; 90 | if (x & 0x80000000000000000000000000 > 0) 91 | result = (result * 0x100000058B90C0B48C6BE5DF846C5B2F0) >> 128; 92 | if (x & 0x40000000000000000000000000 > 0) 93 | result = (result * 0x10000002C5C8601CC6B9E94213C72737B) >> 128; 94 | if (x & 0x20000000000000000000000000 > 0) 95 | result = (result * 0x1000000162E42FFF037DF38AA2B219F07) >> 128; 96 | if (x & 0x10000000000000000000000000 > 0) 97 | result = (result * 0x10000000B17217FBA9C739AA5819F44FA) >> 128; 98 | if (x & 0x8000000000000000000000000 > 0) 99 | result = (result * 0x1000000058B90BFCDEE5ACD3C1CEDC824) >> 128; 100 | if (x & 0x4000000000000000000000000 > 0) 101 | result = (result * 0x100000002C5C85FE31F35A6A30DA1BE51) >> 128; 102 | if (x & 0x2000000000000000000000000 > 0) 103 | result = (result * 0x10000000162E42FF0999CE3541B9FFFD0) >> 128; 104 | if (x & 0x1000000000000000000000000 > 0) 105 | result = (result * 0x100000000B17217F80F4EF5AADDA45554) >> 128; 106 | if (x & 0x800000000000000000000000 > 0) 107 | result = (result * 0x10000000058B90BFBF8479BD5A81B51AE) >> 128; 108 | if (x & 0x400000000000000000000000 > 0) 109 | result = (result * 0x1000000002C5C85FDF84BD62AE30A74CD) >> 128; 110 | if (x & 0x200000000000000000000000 > 0) 111 | result = (result * 0x100000000162E42FEFB2FED257559BDAA) >> 128; 112 | if (x & 0x100000000000000000000000 > 0) 113 | result = (result * 0x1000000000B17217F7D5A7716BBA4A9AF) >> 128; 114 | if (x & 0x80000000000000000000000 > 0) 115 | result = (result * 0x100000000058B90BFBE9DDBAC5E109CCF) >> 128; 116 | if (x & 0x40000000000000000000000 > 0) 117 | result = (result * 0x10000000002C5C85FDF4B15DE6F17EB0E) >> 128; 118 | if (x & 0x20000000000000000000000 > 0) 119 | result = (result * 0x1000000000162E42FEFA494F1478FDE05) >> 128; 120 | if (x & 0x10000000000000000000000 > 0) 121 | result = (result * 0x10000000000B17217F7D20CF927C8E94D) >> 128; 122 | if (x & 0x8000000000000000000000 > 0) 123 | result = (result * 0x1000000000058B90BFBE8F71CB4E4B33E) >> 128; 124 | if (x & 0x4000000000000000000000 > 0) 125 | result = (result * 0x100000000002C5C85FDF477B662B26946) >> 128; 126 | if (x & 0x2000000000000000000000 > 0) 127 | result = (result * 0x10000000000162E42FEFA3AE53369388D) >> 128; 128 | if (x & 0x1000000000000000000000 > 0) 129 | result = (result * 0x100000000000B17217F7D1D351A389D41) >> 128; 130 | if (x & 0x800000000000000000000 > 0) 131 | result = (result * 0x10000000000058B90BFBE8E8B2D3D4EDF) >> 128; 132 | if (x & 0x400000000000000000000 > 0) 133 | result = (result * 0x1000000000002C5C85FDF4741BEA6E77F) >> 128; 134 | if (x & 0x200000000000000000000 > 0) 135 | result = (result * 0x100000000000162E42FEFA39FE95583C3) >> 128; 136 | if (x & 0x100000000000000000000 > 0) 137 | result = (result * 0x1000000000000B17217F7D1CFB72B45E3) >> 128; 138 | if (x & 0x80000000000000000000 > 0) 139 | result = (result * 0x100000000000058B90BFBE8E7CC35C3F2) >> 128; 140 | if (x & 0x40000000000000000000 > 0) 141 | result = (result * 0x10000000000002C5C85FDF473E242EA39) >> 128; 142 | if (x & 0x20000000000000000000 > 0) 143 | result = (result * 0x1000000000000162E42FEFA39F02B772C) >> 128; 144 | if (x & 0x10000000000000000000 > 0) 145 | result = (result * 0x10000000000000B17217F7D1CF7D83C1A) >> 128; 146 | if (x & 0x8000000000000000000 > 0) 147 | result = (result * 0x1000000000000058B90BFBE8E7BDCBE2E) >> 128; 148 | if (x & 0x4000000000000000000 > 0) 149 | result = (result * 0x100000000000002C5C85FDF473DEA871F) >> 128; 150 | if (x & 0x2000000000000000000 > 0) 151 | result = (result * 0x10000000000000162E42FEFA39EF44D92) >> 128; 152 | if (x & 0x1000000000000000000 > 0) 153 | result = (result * 0x100000000000000B17217F7D1CF79E949) >> 128; 154 | if (x & 0x800000000000000000 > 0) 155 | result = (result * 0x10000000000000058B90BFBE8E7BCE545) >> 128; 156 | if (x & 0x400000000000000000 > 0) 157 | result = (result * 0x1000000000000002C5C85FDF473DE6ECA) >> 128; 158 | if (x & 0x200000000000000000 > 0) 159 | result = (result * 0x100000000000000162E42FEFA39EF366F) >> 128; 160 | if (x & 0x100000000000000000 > 0) 161 | result = (result * 0x1000000000000000B17217F7D1CF79AFA) >> 128; 162 | if (x & 0x80000000000000000 > 0) 163 | result = (result * 0x100000000000000058B90BFBE8E7BCD6E) >> 128; 164 | if (x & 0x40000000000000000 > 0) 165 | result = (result * 0x10000000000000002C5C85FDF473DE6B3) >> 128; 166 | if (x & 0x20000000000000000 > 0) 167 | result = (result * 0x1000000000000000162E42FEFA39EF359) >> 128; 168 | if (x & 0x10000000000000000 > 0) 169 | result = (result * 0x10000000000000000B17217F7D1CF79AC) >> 128; 170 | 171 | // We're doing two things at the same time: 172 | // 173 | // 1. Multiply the result by 2^n + 1, where 2^n is the integer part and 1 is an extra bit to account 174 | // for the fact that we initially set the result to 0.5 We implement this by subtracting from 127 175 | // instead of 128. 176 | // 2. Convert the result to the unsigned 60.18-decimal fixed-point format. 177 | // 178 | // This works because result * SCALE * 2^ip / 2^127 = result * SCALE / 2^(127 - ip), where ip is the integer 179 | // part and SCALE / 2^128 is what converts the result to the unsigned fixed-point format. 180 | result *= SCALE; 181 | result >>= (127 - (x >> 128)); 182 | } 183 | } 184 | 185 | /// @notice Finds the zero-based index of the first one in the binary representation of x. 186 | /// @dev See the note on msb in the "Find First Set" Wikipedia article https://en.wikipedia.org/wiki/Find_first_set 187 | /// @param x The uint256 number for which to find the index of the most significant bit. 188 | /// @return msb The index of the most significant bit as an uint256. 189 | function mostSignificantBit(uint256 x) internal pure returns (uint256 msb) { 190 | if (x >= 2**128) { 191 | x >>= 128; 192 | msb += 128; 193 | } 194 | if (x >= 2**64) { 195 | x >>= 64; 196 | msb += 64; 197 | } 198 | if (x >= 2**32) { 199 | x >>= 32; 200 | msb += 32; 201 | } 202 | if (x >= 2**16) { 203 | x >>= 16; 204 | msb += 16; 205 | } 206 | if (x >= 2**8) { 207 | x >>= 8; 208 | msb += 8; 209 | } 210 | if (x >= 2**4) { 211 | x >>= 4; 212 | msb += 4; 213 | } 214 | if (x >= 2**2) { 215 | x >>= 2; 216 | msb += 2; 217 | } 218 | if (x >= 2**1) { 219 | // No need to shift x any more. 220 | msb += 1; 221 | } 222 | } 223 | 224 | /// @notice Calculates floor(x*y÷denominator) with full precision. 225 | /// 226 | /// @dev Credit to Remco Bloemen under MIT license https://xn--2-umb.com/21/muldiv. 227 | /// 228 | /// Requirements: 229 | /// - The denominator cannot be zero. 230 | /// - The result must fit within uint256. 231 | /// 232 | /// Caveats: 233 | /// - This function does not work with fixed-point numbers. 234 | /// 235 | /// @param x The multiplicand as an uint256. 236 | /// @param y The multiplier as an uint256. 237 | /// @param denominator The divisor as an uint256. 238 | /// @return result The result as an uint256. 239 | function mulDiv( 240 | uint256 x, 241 | uint256 y, 242 | uint256 denominator 243 | ) internal pure returns (uint256 result) { 244 | // 512-bit multiply [prod1 prod0] = x * y. Compute the product mod 2**256 and mod 2**256 - 1, then use 245 | // use the Chinese Remainder Theorem to reconstruct the 512 bit result. The result is stored in two 256 246 | // variables such that product = prod1 * 2**256 + prod0. 247 | uint256 prod0; // Least significant 256 bits of the product 248 | uint256 prod1; // Most significant 256 bits of the product 249 | assembly { 250 | let mm := mulmod(x, y, not(0)) 251 | prod0 := mul(x, y) 252 | prod1 := sub(sub(mm, prod0), lt(mm, prod0)) 253 | } 254 | 255 | // Handle non-overflow cases, 256 by 256 division 256 | if (prod1 == 0) { 257 | require(denominator > 0); 258 | assembly { 259 | result := div(prod0, denominator) 260 | } 261 | return result; 262 | } 263 | 264 | // Make sure the result is less than 2**256. Also prevents denominator == 0. 265 | require(denominator > prod1); 266 | 267 | /////////////////////////////////////////////// 268 | // 512 by 256 division. 269 | /////////////////////////////////////////////// 270 | 271 | // Make division exact by subtracting the remainder from [prod1 prod0]. 272 | uint256 remainder; 273 | assembly { 274 | // Compute remainder using mulmod. 275 | remainder := mulmod(x, y, denominator) 276 | 277 | // Subtract 256 bit number from 512 bit number 278 | prod1 := sub(prod1, gt(remainder, prod0)) 279 | prod0 := sub(prod0, remainder) 280 | } 281 | 282 | // Factor powers of two out of denominator and compute largest power of two divisor of denominator. Always >= 1. 283 | // See https://cs.stackexchange.com/q/138556/92363. 284 | unchecked { 285 | // Does not overflow because the denominator cannot be zero at this stage in the function. 286 | uint256 lpotdod = denominator & (~denominator + 1); 287 | assembly { 288 | // Divide denominator by lpotdod. 289 | denominator := div(denominator, lpotdod) 290 | 291 | // Divide [prod1 prod0] by lpotdod. 292 | prod0 := div(prod0, lpotdod) 293 | 294 | // Flip lpotdod such that it is 2**256 / lpotdod. If lpotdod is zero, then it becomes one. 295 | lpotdod := add(div(sub(0, lpotdod), lpotdod), 1) 296 | } 297 | 298 | // Shift in bits from prod1 into prod0. 299 | prod0 |= prod1 * lpotdod; 300 | 301 | // Invert denominator mod 2**256. Now that denominator is an odd number, it has an inverse modulo 2**256 such 302 | // that denominator * inv = 1 mod 2**256. Compute the inverse by starting with a seed that is correct for 303 | // four bits. That is, denominator * inv = 1 mod 2**4 304 | uint256 inverse = (3 * denominator) ^ 2; 305 | 306 | // Now use Newton-Raphson iteration to improve the precision. Thanks to Hensel's lifting lemma, this also works 307 | // in modular arithmetic, doubling the correct bits in each step. 308 | inverse *= 2 - denominator * inverse; // inverse mod 2**8 309 | inverse *= 2 - denominator * inverse; // inverse mod 2**16 310 | inverse *= 2 - denominator * inverse; // inverse mod 2**32 311 | inverse *= 2 - denominator * inverse; // inverse mod 2**64 312 | inverse *= 2 - denominator * inverse; // inverse mod 2**128 313 | inverse *= 2 - denominator * inverse; // inverse mod 2**256 314 | 315 | // Because the division is now exact we can divide by multiplying with the modular inverse of denominator. 316 | // This will give us the correct result modulo 2**256. Since the precoditions guarantee that the outcome is 317 | // less than 2**256, this is the final result. We don't need to compute the high bits of the result and prod1 318 | // is no longer required. 319 | result = prod0 * inverse; 320 | return result; 321 | } 322 | } 323 | 324 | /// @notice Calculates floor(x*y÷1e18) with full precision. 325 | /// 326 | /// @dev Variant of "mulDiv" with constant folding, i.e. in which the denominator is always 1e18. Before returning the 327 | /// final result, we add 1 if (x * y) % SCALE >= HALF_SCALE. Without this, 6.6e-19 would be truncated to 0 instead of 328 | /// being rounded to 1e-18. See "Listing 6" and text above it at https://accu.org/index.php/journals/1717. 329 | /// 330 | /// Requirements: 331 | /// - The result must fit within uint256. 332 | /// 333 | /// Caveats: 334 | /// - The body is purposely left uncommented; see the NatSpec comments in "PRBMath.mulDiv" to understand how this works. 335 | /// - It is assumed that the result can never be type(uint256).max when x and y solve the following two queations: 336 | /// 1. x * y = type(uint256).max * SCALE 337 | /// 2. (x * y) % SCALE >= SCALE / 2 338 | /// 339 | /// @param x The multiplicand as an unsigned 60.18-decimal fixed-point number. 340 | /// @param y The multiplier as an unsigned 60.18-decimal fixed-point number. 341 | /// @return result The result as an unsigned 60.18-decimal fixed-point number. 342 | function mulDivFixedPoint(uint256 x, uint256 y) 343 | internal 344 | pure 345 | returns (uint256 result) 346 | { 347 | uint256 prod0; 348 | uint256 prod1; 349 | assembly { 350 | let mm := mulmod(x, y, not(0)) 351 | prod0 := mul(x, y) 352 | prod1 := sub(sub(mm, prod0), lt(mm, prod0)) 353 | } 354 | 355 | uint256 remainder; 356 | uint256 roundUpUnit; 357 | assembly { 358 | remainder := mulmod(x, y, SCALE) 359 | roundUpUnit := gt(remainder, 499999999999999999) 360 | } 361 | 362 | if (prod1 == 0) { 363 | unchecked { 364 | result = (prod0 / SCALE) + roundUpUnit; 365 | return result; 366 | } 367 | } 368 | 369 | require(SCALE > prod1); 370 | 371 | assembly { 372 | result := add( 373 | mul( 374 | or( 375 | div(sub(prod0, remainder), SCALE_LPOTD), 376 | mul( 377 | sub(prod1, gt(remainder, prod0)), 378 | add(div(sub(0, SCALE_LPOTD), SCALE_LPOTD), 1) 379 | ) 380 | ), 381 | SCALE_INVERSE 382 | ), 383 | roundUpUnit 384 | ) 385 | } 386 | } 387 | 388 | /// @notice Calculates floor(x*y÷denominator) with full precision. 389 | /// 390 | /// @dev An extension of "mulDiv" for signed numbers. Works by computing the signs and the absolute values separately. 391 | /// 392 | /// Requirements: 393 | /// - None of the inputs can be type(int256).min. 394 | /// - The result must fit within int256. 395 | /// 396 | /// @param x The multiplicand as an int256. 397 | /// @param y The multiplier as an int256. 398 | /// @param denominator The divisor as an int256. 399 | /// @return result The result as an int256. 400 | function mulDivSigned( 401 | int256 x, 402 | int256 y, 403 | int256 denominator 404 | ) internal pure returns (int256 result) { 405 | require(x > type(int256).min); 406 | require(y > type(int256).min); 407 | require(denominator > type(int256).min); 408 | 409 | // Get hold of the absolute values of x, y and the denominator. 410 | uint256 ax; 411 | uint256 ay; 412 | uint256 ad; 413 | unchecked { 414 | ax = x < 0 ? uint256(-x) : uint256(x); 415 | ay = y < 0 ? uint256(-y) : uint256(y); 416 | ad = denominator < 0 ? uint256(-denominator) : uint256(denominator); 417 | } 418 | 419 | // Compute the absolute value of (x*y)÷denominator. The result must fit within int256. 420 | uint256 resultUnsigned = mulDiv(ax, ay, ad); 421 | require(resultUnsigned <= uint256(type(int256).max)); 422 | 423 | // Get the signs of x, y and the denominator. 424 | uint256 sx; 425 | uint256 sy; 426 | uint256 sd; 427 | assembly { 428 | sx := sgt(x, sub(0, 1)) 429 | sy := sgt(y, sub(0, 1)) 430 | sd := sgt(denominator, sub(0, 1)) 431 | } 432 | 433 | // XOR over sx, sy and sd. This is checking whether there are one or three negative signs in the inputs. 434 | // If yes, the result should be negative. 435 | result = sx ^ sy ^ sd == 0 436 | ? -int256(resultUnsigned) 437 | : int256(resultUnsigned); 438 | } 439 | 440 | /// @notice Calculates the square root of x, rounding down. 441 | /// @dev Uses the Babylonian method https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method. 442 | /// 443 | /// Caveats: 444 | /// - This function does not work with fixed-point numbers. 445 | /// 446 | /// @param x The uint256 number for which to calculate the square root. 447 | /// @return result The result as an uint256. 448 | function sqrt(uint256 x) internal pure returns (uint256 result) { 449 | if (x == 0) { 450 | return 0; 451 | } 452 | 453 | // Set the initial guess to the closest power of two that is higher than x. 454 | uint256 xAux = uint256(x); 455 | result = 1; 456 | if (xAux >= 0x100000000000000000000000000000000) { 457 | xAux >>= 128; 458 | result <<= 64; 459 | } 460 | if (xAux >= 0x10000000000000000) { 461 | xAux >>= 64; 462 | result <<= 32; 463 | } 464 | if (xAux >= 0x100000000) { 465 | xAux >>= 32; 466 | result <<= 16; 467 | } 468 | if (xAux >= 0x10000) { 469 | xAux >>= 16; 470 | result <<= 8; 471 | } 472 | if (xAux >= 0x100) { 473 | xAux >>= 8; 474 | result <<= 4; 475 | } 476 | if (xAux >= 0x10) { 477 | xAux >>= 4; 478 | result <<= 2; 479 | } 480 | if (xAux >= 0x8) { 481 | result <<= 1; 482 | } 483 | 484 | // The operations can never overflow because the result is max 2^127 when it enters this block. 485 | unchecked { 486 | result = (result + x / result) >> 1; 487 | result = (result + x / result) >> 1; 488 | result = (result + x / result) >> 1; 489 | result = (result + x / result) >> 1; 490 | result = (result + x / result) >> 1; 491 | result = (result + x / result) >> 1; 492 | result = (result + x / result) >> 1; // Seven iterations should be enough 493 | uint256 roundedDownResult = x / result; 494 | return result >= roundedDownResult ? roundedDownResult : result; 495 | } 496 | } 497 | } 498 | -------------------------------------------------------------------------------- /contracts/PRBMathUD60x18.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: WTFPL 2 | pragma solidity >=0.8.0; 3 | 4 | import "./PRBMath.sol"; 5 | 6 | /// @title PRBMathUD60x18 7 | /// @author Paul Razvan Berg 8 | /// @notice Smart contract library for advanced fixed-point math. It works with uint256 numbers considered to have 18 9 | /// trailing decimals. We call this number representation unsigned 60.18-decimal fixed-point, since there can be up to 60 10 | /// digits in the integer part and up to 18 decimals in the fractional part. The numbers are bound by the minimum and the 11 | /// maximum values permitted by the Solidity type uint256. 12 | library PRBMathUD60x18 { 13 | /// STORAGE /// 14 | 15 | /// @dev Half the SCALE number. 16 | uint256 internal constant HALF_SCALE = 5e17; 17 | 18 | /// @dev log2(e) as an unsigned 60.18-decimal fixed-point number. 19 | uint256 internal constant LOG2_E = 1442695040888963407; 20 | 21 | /// @dev The maximum value an unsigned 60.18-decimal fixed-point number can have. 22 | uint256 internal constant MAX_UD60x18 = 23 | 115792089237316195423570985008687907853269984665640564039457584007913129639935; 24 | 25 | /// @dev The maximum whole value an unsigned 60.18-decimal fixed-point number can have. 26 | uint256 internal constant MAX_WHOLE_UD60x18 = 27 | 115792089237316195423570985008687907853269984665640564039457000000000000000000; 28 | 29 | /// @dev How many trailing decimals can be represented. 30 | uint256 internal constant SCALE = 1e18; 31 | 32 | /// @notice Adds two unsigned 60.18-decimal fixed-point numbers together, returning a new unsigned 60.18-decimal 33 | /// fixed-point number. 34 | /// @param x The first unsigned 60.18-decimal fixed-point number to add. 35 | /// @param y The second unsigned 60.18-decimal fixed-point number to add. 36 | /// @param result The result as an unsigned 59.18 decimal fixed-point number. 37 | function add(PRBMath.UD60x18 memory x, PRBMath.UD60x18 memory y) 38 | internal 39 | pure 40 | returns (PRBMath.UD60x18 memory result) 41 | { 42 | unchecked { 43 | uint256 rValue = x.value + y.value; 44 | require(rValue >= x.value); 45 | result = PRBMath.UD60x18({value: rValue}); 46 | } 47 | } 48 | 49 | /// @notice Calculates arithmetic average of x and y, rounding down. 50 | /// @param x The first operand as an unsigned 60.18-decimal fixed-point number. 51 | /// @param y The second operand as an unsigned 60.18-decimal fixed-point number. 52 | /// @return result The arithmetic average as an usigned 60.18-decimal fixed-point number. 53 | function avg(PRBMath.UD60x18 memory x, PRBMath.UD60x18 memory y) 54 | internal 55 | pure 56 | returns (PRBMath.UD60x18 memory result) 57 | { 58 | // The operations can never overflow. 59 | unchecked { 60 | // The last operand checks if both x and y are odd and if that is the case, we add 1 to the result. We need 61 | // to do this because if both numbers are odd, the 0.5 remainder gets truncated twice. 62 | uint256 rValue = (x.value >> 1) + 63 | (y.value >> 1) + 64 | (x.value & y.value & 1); 65 | result = PRBMath.UD60x18({value: rValue}); 66 | } 67 | } 68 | 69 | /// @notice Yields the least unsigned 60.18 decimal fixed-point number greater than or equal to x. 70 | /// 71 | /// @dev Optimised for fractional value inputs, because for every whole value there are (1e18 - 1) fractional counterparts. 72 | /// See https://en.wikipedia.org/wiki/Floor_and_ceiling_functions. 73 | /// 74 | /// Requirements: 75 | /// - x must be less than or equal to MAX_WHOLE_UD60x18. 76 | /// 77 | /// @param x The unsigned 60.18-decimal fixed-point number to ceil. 78 | /// @param result The least integer greater than or equal to x, as an unsigned 60.18-decimal fixed-point number. 79 | function ceil(PRBMath.UD60x18 memory x) 80 | internal 81 | pure 82 | returns (PRBMath.UD60x18 memory result) 83 | { 84 | uint256 xValue = x.value; 85 | require(xValue <= MAX_WHOLE_UD60x18); 86 | 87 | uint256 rValue; 88 | assembly { 89 | // Equivalent to "x % SCALE" but faster. 90 | let remainder := mod(xValue, SCALE) 91 | 92 | // Equivalent to "SCALE - remainder" but faster. 93 | let delta := sub(SCALE, remainder) 94 | 95 | // Equivalent to "x + delta * (remainder > 0 ? 1 : 0)" but faster. 96 | rValue := add(xValue, mul(delta, gt(remainder, 0))) 97 | } 98 | result = PRBMath.UD60x18({value: rValue}); 99 | } 100 | 101 | /// @notice Divides two unsigned 60.18-decimal fixed-point numbers, returning a new unsigned 60.18-decimal fixed-point number. 102 | /// 103 | /// @dev Uses mulDiv to enable overflow-safe multiplication and division. 104 | /// 105 | /// Requirements: 106 | /// - y cannot be zero. 107 | /// 108 | /// @param x The numerator as an unsigned 60.18-decimal fixed-point number. 109 | /// @param y The denominator as an unsigned 60.18-decimal fixed-point number. 110 | /// @param result The quotient as an unsigned 60.18-decimal fixed-point number. 111 | function div(PRBMath.UD60x18 memory x, PRBMath.UD60x18 memory y) 112 | internal 113 | pure 114 | returns (PRBMath.UD60x18 memory result) 115 | { 116 | result = PRBMath.UD60x18({ 117 | value: PRBMath.mulDiv(x.value, SCALE, y.value) 118 | }); 119 | } 120 | 121 | /// @notice Returns Euler's number as an unsigned 60.18-decimal fixed-point number. 122 | /// @dev See https://en.wikipedia.org/wiki/E_(mathematical_constant). 123 | function e() internal pure returns (PRBMath.UD60x18 memory result) { 124 | result = PRBMath.UD60x18({value: 2718281828459045235}); 125 | } 126 | 127 | /// @notice Calculates the natural exponent of x. 128 | /// 129 | /// @dev Based on the insight that e^x = 2^(x * log2(e)). 130 | /// 131 | /// Requirements: 132 | /// - All from "log2". 133 | /// - x must be less than 88.722839111672999628. 134 | /// 135 | /// @param x The exponent as an unsigned 60.18-decimal fixed-point number. 136 | /// @return result The result as an unsigned 60.18-decimal fixed-point number. 137 | function exp(PRBMath.UD60x18 memory x) 138 | internal 139 | pure 140 | returns (PRBMath.UD60x18 memory result) 141 | { 142 | // Without this check, the value passed to "exp2" would be greater than 128e18. 143 | require(x.value < 88722839111672999628); 144 | 145 | // Do the fixed-point multiplication inline to save gas. 146 | unchecked { 147 | uint256 doubleScaleProduct = x.value * LOG2_E; 148 | PRBMath.UD60x18 memory exponent = PRBMath.UD60x18({ 149 | value: (doubleScaleProduct + HALF_SCALE) / SCALE 150 | }); 151 | result = exp2(exponent); 152 | } 153 | } 154 | 155 | /// @notice Calculates the binary exponent of x using the binary fraction method. 156 | /// 157 | /// @dev See https://ethereum.stackexchange.com/q/79903/24693. 158 | /// 159 | /// Requirements: 160 | /// - x must be 128e18 or less. 161 | /// - The result must fit within MAX_UD60x18. 162 | /// 163 | /// @param x The exponent as an unsigned 60.18-decimal fixed-point number. 164 | /// @return result The result as an unsigned 60.18-decimal fixed-point number. 165 | function exp2(PRBMath.UD60x18 memory x) 166 | internal 167 | pure 168 | returns (PRBMath.UD60x18 memory result) 169 | { 170 | // 2**128 doesn't fit within the 128.128-bit format used internally in this function. 171 | require(x.value < 128e18); 172 | 173 | unchecked { 174 | // Convert x to the 128.128-bit fixed-point format. 175 | uint256 x128x128 = (x.value << 128) / SCALE; 176 | 177 | // Pass x to the PRBMath.exp2 function, which uses the 128.128-bit fixed-point number representation. 178 | result = PRBMath.UD60x18({value: PRBMath.exp2(x128x128)}); 179 | } 180 | } 181 | 182 | /// @notice Yields the greatest unsigned 60.18 decimal fixed-point number less than or equal to x. 183 | /// @dev Optimised for fractional value inputs, because for every whole value there are (1e18 - 1) fractional counterparts. 184 | /// See https://en.wikipedia.org/wiki/Floor_and_ceiling_functions. 185 | /// @param x The unsigned 60.18-decimal fixed-point number to floor. 186 | /// @param result The greatest integer less than or equal to x, as an unsigned 60.18-decimal fixed-point number. 187 | function floor(PRBMath.UD60x18 memory x) 188 | internal 189 | pure 190 | returns (PRBMath.UD60x18 memory result) 191 | { 192 | uint256 xValue = x.value; 193 | uint256 rValue; 194 | assembly { 195 | // Equivalent to "x % SCALE" but faster. 196 | let remainder := mod(xValue, SCALE) 197 | 198 | // Equivalent to "x - remainder * (remainder > 0 ? 1 : 0)" but faster. 199 | rValue := sub(xValue, mul(remainder, gt(remainder, 0))) 200 | } 201 | result = PRBMath.UD60x18({value: rValue}); 202 | } 203 | 204 | /// @notice Yields the excess beyond the floor of x. 205 | /// @dev Based on the odd function definition https://en.wikipedia.org/wiki/Fractional_part. 206 | /// @param x The unsigned 60.18-decimal fixed-point number to get the fractional part of. 207 | /// @param result The fractional part of x as an unsigned 60.18-decimal fixed-point number. 208 | function frac(PRBMath.UD60x18 memory x) 209 | internal 210 | pure 211 | returns (PRBMath.UD60x18 memory result) 212 | { 213 | uint256 xValue = x.value; 214 | uint256 rValue; 215 | assembly { 216 | rValue := mod(xValue, SCALE) 217 | } 218 | result = PRBMath.UD60x18({value: rValue}); 219 | } 220 | 221 | /// @notice Converts a number from basic integer form to unsigned 60.18-decimal fixed-point representation. 222 | /// 223 | /// @dev Requirements: 224 | /// - x must be less than or equal to MAX_UD60x18 divided by SCALE. 225 | /// 226 | /// @param x The basic integer to convert. 227 | /// @param result The same number in unsigned 60.18-decimal fixed-point representation. 228 | function fromUint(uint256 x) 229 | internal 230 | pure 231 | returns (PRBMath.UD60x18 memory result) 232 | { 233 | unchecked { 234 | require(x <= MAX_UD60x18 / SCALE); 235 | result = PRBMath.UD60x18({value: x * SCALE}); 236 | } 237 | } 238 | 239 | /// @notice Calculates geometric mean of x and y, i.e. sqrt(x * y), rounding down. 240 | /// 241 | /// @dev Requirements: 242 | /// - x * y must fit within MAX_UD60x18, lest it overflows. 243 | /// 244 | /// @param x The first operand as an unsigned 60.18-decimal fixed-point number. 245 | /// @param y The second operand as an unsigned 60.18-decimal fixed-point number. 246 | /// @return result The result as an unsigned 60.18-decimal fixed-point number. 247 | function gm(PRBMath.UD60x18 memory x, PRBMath.UD60x18 memory y) 248 | internal 249 | pure 250 | returns (PRBMath.UD60x18 memory result) 251 | { 252 | if (x.value == 0) { 253 | return PRBMath.UD60x18({value: 0}); 254 | } 255 | 256 | unchecked { 257 | // Checking for overflow this way is faster than letting Solidity do it. 258 | uint256 xy = x.value * y.value; 259 | require(xy / x.value == y.value); 260 | 261 | // We don't need to multiply by the SCALE here because the x*y product had already picked up a factor of SCALE 262 | // during multiplication. See the comments within the "sqrt" function. 263 | result = PRBMath.UD60x18({value: PRBMath.sqrt(xy)}); 264 | } 265 | } 266 | 267 | /// @notice Calculates 1 / x, rounding towards zero. 268 | /// 269 | /// @dev Requirements: 270 | /// - x cannot be zero. 271 | /// 272 | /// @param x The unsigned 60.18-decimal fixed-point number for which to calculate the inverse. 273 | /// @return result The inverse as an unsigned 60.18-decimal fixed-point number. 274 | function inv(PRBMath.UD60x18 memory x) 275 | internal 276 | pure 277 | returns (PRBMath.UD60x18 memory result) 278 | { 279 | unchecked { 280 | // 1e36 is SCALE * SCALE. 281 | result = PRBMath.UD60x18({value: 1e36 / x.value}); 282 | } 283 | } 284 | 285 | /// @notice Calculates the natural logarithm of x. 286 | /// 287 | /// @dev Based on the insight that ln(x) = log2(x) / log2(e). 288 | /// 289 | /// Requirements: 290 | /// - All from "log2". 291 | /// 292 | /// Caveats: 293 | /// - All from "log2". 294 | /// - This doesn't return exactly 1 for 2.718281828459045235, for that we would need more fine-grained precision. 295 | /// 296 | /// @param x The unsigned 60.18-decimal fixed-point number for which to calculate the natural logarithm. 297 | /// @return result The natural logarithm as an unsigned 60.18-decimal fixed-point number. 298 | function ln(PRBMath.UD60x18 memory x) 299 | internal 300 | pure 301 | returns (PRBMath.UD60x18 memory result) 302 | { 303 | // Do the fixed-point multiplication inline to save gas. This is overflow-safe because the maximum value that log2(x) 304 | // can return is 196205294292027477728. 305 | unchecked { 306 | uint256 rValue = (log2(x).value * SCALE) / LOG2_E; 307 | result = PRBMath.UD60x18({value: rValue}); 308 | } 309 | } 310 | 311 | /// @notice Calculates the common logarithm of x. 312 | /// 313 | /// @dev First checks if x is an exact power of ten and it stops if yes. If it's not, calculates the common 314 | /// logarithm based on the insight that log10(x) = log2(x) / log2(10). 315 | /// 316 | /// Requirements: 317 | /// - All from "log2". 318 | /// 319 | /// Caveats: 320 | /// - All from "log2". 321 | /// 322 | /// @param x The unsigned 60.18-decimal fixed-point number for which to calculate the common logarithm. 323 | /// @return result The common logarithm as an unsigned 60.18-decimal fixed-point number. 324 | function log10(PRBMath.UD60x18 memory x) 325 | internal 326 | pure 327 | returns (PRBMath.UD60x18 memory result) 328 | { 329 | uint256 xValue = x.value; 330 | require(xValue >= SCALE); 331 | 332 | // Note that the "mul" in this block is the assembly mul operation, not the "mul" function defined in this 333 | // contract. 334 | uint256 rValue; 335 | 336 | // prettier-ignore 337 | assembly { 338 | switch x 339 | case 1 { rValue := mul(SCALE, sub(0, 18)) } 340 | case 10 { rValue := mul(SCALE, sub(1, 18)) } 341 | case 100 { rValue := mul(SCALE, sub(2, 18)) } 342 | case 1000 { rValue := mul(SCALE, sub(3, 18)) } 343 | case 10000 { rValue := mul(SCALE, sub(4, 18)) } 344 | case 100000 { rValue := mul(SCALE, sub(5, 18)) } 345 | case 1000000 { rValue := mul(SCALE, sub(6, 18)) } 346 | case 10000000 { rValue := mul(SCALE, sub(7, 18)) } 347 | case 100000000 { rValue := mul(SCALE, sub(8, 18)) } 348 | case 1000000000 { rValue := mul(SCALE, sub(9, 18)) } 349 | case 10000000000 { rValue := mul(SCALE, sub(10, 18)) } 350 | case 100000000000 { rValue := mul(SCALE, sub(11, 18)) } 351 | case 1000000000000 { rValue := mul(SCALE, sub(12, 18)) } 352 | case 10000000000000 { rValue := mul(SCALE, sub(13, 18)) } 353 | case 100000000000000 { rValue := mul(SCALE, sub(14, 18)) } 354 | case 1000000000000000 { rValue := mul(SCALE, sub(15, 18)) } 355 | case 10000000000000000 { rValue := mul(SCALE, sub(16, 18)) } 356 | case 100000000000000000 { rValue := mul(SCALE, sub(17, 18)) } 357 | case 1000000000000000000 { rValue := 0 } 358 | case 10000000000000000000 { rValue := SCALE } 359 | case 100000000000000000000 { rValue := mul(SCALE, 2) } 360 | case 1000000000000000000000 { rValue := mul(SCALE, 3) } 361 | case 10000000000000000000000 { rValue := mul(SCALE, 4) } 362 | case 100000000000000000000000 { rValue := mul(SCALE, 5) } 363 | case 1000000000000000000000000 { rValue := mul(SCALE, 6) } 364 | case 10000000000000000000000000 { rValue := mul(SCALE, 7) } 365 | case 100000000000000000000000000 { rValue := mul(SCALE, 8) } 366 | case 1000000000000000000000000000 { rValue := mul(SCALE, 9) } 367 | case 10000000000000000000000000000 { rValue := mul(SCALE, 10) } 368 | case 100000000000000000000000000000 { rValue := mul(SCALE, 11) } 369 | case 1000000000000000000000000000000 { rValue := mul(SCALE, 12) } 370 | case 10000000000000000000000000000000 { rValue := mul(SCALE, 13) } 371 | case 100000000000000000000000000000000 { rValue := mul(SCALE, 14) } 372 | case 1000000000000000000000000000000000 { rValue := mul(SCALE, 15) } 373 | case 10000000000000000000000000000000000 { rValue := mul(SCALE, 16) } 374 | case 100000000000000000000000000000000000 { rValue := mul(SCALE, 17) } 375 | case 1000000000000000000000000000000000000 { rValue := mul(SCALE, 18) } 376 | case 10000000000000000000000000000000000000 { rValue := mul(SCALE, 19) } 377 | case 100000000000000000000000000000000000000 { rValue := mul(SCALE, 20) } 378 | case 1000000000000000000000000000000000000000 { rValue := mul(SCALE, 21) } 379 | case 10000000000000000000000000000000000000000 { rValue := mul(SCALE, 22) } 380 | case 100000000000000000000000000000000000000000 { rValue := mul(SCALE, 23) } 381 | case 1000000000000000000000000000000000000000000 { rValue := mul(SCALE, 24) } 382 | case 10000000000000000000000000000000000000000000 { rValue := mul(SCALE, 25) } 383 | case 100000000000000000000000000000000000000000000 { rValue := mul(SCALE, 26) } 384 | case 1000000000000000000000000000000000000000000000 { rValue := mul(SCALE, 27) } 385 | case 10000000000000000000000000000000000000000000000 { rValue := mul(SCALE, 28) } 386 | case 100000000000000000000000000000000000000000000000 { rValue := mul(SCALE, 29) } 387 | case 1000000000000000000000000000000000000000000000000 { rValue := mul(SCALE, 30) } 388 | case 10000000000000000000000000000000000000000000000000 { rValue := mul(SCALE, 31) } 389 | case 100000000000000000000000000000000000000000000000000 { rValue := mul(SCALE, 32) } 390 | case 1000000000000000000000000000000000000000000000000000 { rValue := mul(SCALE, 33) } 391 | case 10000000000000000000000000000000000000000000000000000 { rValue := mul(SCALE, 34) } 392 | case 100000000000000000000000000000000000000000000000000000 { rValue := mul(SCALE, 35) } 393 | case 1000000000000000000000000000000000000000000000000000000 { rValue := mul(SCALE, 36) } 394 | case 10000000000000000000000000000000000000000000000000000000 { rValue := mul(SCALE, 37) } 395 | case 100000000000000000000000000000000000000000000000000000000 { rValue := mul(SCALE, 38) } 396 | case 1000000000000000000000000000000000000000000000000000000000 { rValue := mul(SCALE, 39) } 397 | case 10000000000000000000000000000000000000000000000000000000000 { rValue := mul(SCALE, 40) } 398 | case 100000000000000000000000000000000000000000000000000000000000 { rValue := mul(SCALE, 41) } 399 | case 1000000000000000000000000000000000000000000000000000000000000 { rValue := mul(SCALE, 42) } 400 | case 10000000000000000000000000000000000000000000000000000000000000 { rValue := mul(SCALE, 43) } 401 | case 100000000000000000000000000000000000000000000000000000000000000 { rValue := mul(SCALE, 44) } 402 | case 1000000000000000000000000000000000000000000000000000000000000000 { rValue := mul(SCALE, 45) } 403 | case 10000000000000000000000000000000000000000000000000000000000000000 { rValue := mul(SCALE, 46) } 404 | case 100000000000000000000000000000000000000000000000000000000000000000 { rValue := mul(SCALE, 47) } 405 | case 1000000000000000000000000000000000000000000000000000000000000000000 { rValue := mul(SCALE, 48) } 406 | case 10000000000000000000000000000000000000000000000000000000000000000000 { rValue := mul(SCALE, 49) } 407 | case 100000000000000000000000000000000000000000000000000000000000000000000 { rValue := mul(SCALE, 50) } 408 | case 1000000000000000000000000000000000000000000000000000000000000000000000 { rValue := mul(SCALE, 51) } 409 | case 10000000000000000000000000000000000000000000000000000000000000000000000 { rValue := mul(SCALE, 52) } 410 | case 100000000000000000000000000000000000000000000000000000000000000000000000 { rValue := mul(SCALE, 53) } 411 | case 1000000000000000000000000000000000000000000000000000000000000000000000000 { rValue := mul(SCALE, 54) } 412 | case 10000000000000000000000000000000000000000000000000000000000000000000000000 { rValue := mul(SCALE, 55) } 413 | case 100000000000000000000000000000000000000000000000000000000000000000000000000 { rValue := mul(SCALE, 56) } 414 | case 1000000000000000000000000000000000000000000000000000000000000000000000000000 { rValue := mul(SCALE, 57) } 415 | case 10000000000000000000000000000000000000000000000000000000000000000000000000000 { rValue := mul(SCALE, 58) } 416 | case 100000000000000000000000000000000000000000000000000000000000000000000000000000 { rValue := mul(SCALE, 59) } 417 | default { 418 | rValue := MAX_UD60x18 419 | } 420 | } 421 | 422 | if (rValue != MAX_UD60x18) { 423 | result = PRBMath.UD60x18({value: rValue}); 424 | } else { 425 | // Do the fixed-point division inline to save gas. The denominator is log2(10). 426 | unchecked { 427 | rValue = (log2(x).value * SCALE) / 3321928094887362347; 428 | result = PRBMath.UD60x18({value: rValue}); 429 | } 430 | } 431 | } 432 | 433 | /// @notice Calculates the binary logarithm of x. 434 | /// 435 | /// @dev Based on the iterative approximation algorithm. 436 | /// https://en.wikipedia.org/wiki/Binary_logarithm#Iterative_approximation 437 | /// 438 | /// Requirements: 439 | /// - x must be greater than or equal to SCALE, otherwise the result would be negative. 440 | /// 441 | /// Caveats: 442 | /// - The results are nor perfectly accurate to the last decimal, due to the lossy precision of the iterative approximation. 443 | /// 444 | /// @param x The unsigned 60.18-decimal fixed-point number for which to calculate the binary logarithm. 445 | /// @return result The binary logarithm as an unsigned 60.18-decimal fixed-point number. 446 | function log2(PRBMath.UD60x18 memory x) 447 | internal 448 | pure 449 | returns (PRBMath.UD60x18 memory result) 450 | { 451 | require(x.value >= SCALE); 452 | unchecked { 453 | // Calculate the integer part of the logarithm and add it to the result and finally calculate y = x * 2^(-n). 454 | uint256 n = PRBMath.mostSignificantBit(x.value / SCALE); 455 | 456 | // The integer part of the logarithm as an unsigned 60.18-decimal fixed-point number. The operation can't overflow 457 | // because n is maximum 255 and SCALE is 1e18. 458 | uint256 rValue = n * SCALE; 459 | 460 | // This is y = x * 2^(-n). 461 | uint256 y = x.value >> n; 462 | 463 | // If y = 1, the fractional part is zero. 464 | if (y == SCALE) { 465 | return PRBMath.UD60x18({value: rValue}); 466 | } 467 | 468 | // Calculate the fractional part via the iterative approximation. 469 | // The "delta >>= 1" part is equivalent to "delta /= 2", but shifting bits is faster. 470 | for (uint256 delta = HALF_SCALE; delta > 0; delta >>= 1) { 471 | y = (y * y) / SCALE; 472 | 473 | // Is y^2 > 2 and so in the range [2,4)? 474 | if (y >= 2 * SCALE) { 475 | // Add the 2^(-m) factor to the logarithm. 476 | rValue += delta; 477 | 478 | // Corresponds to z/2 on Wikipedia. 479 | y >>= 1; 480 | } 481 | } 482 | result = PRBMath.UD60x18({value: rValue}); 483 | } 484 | } 485 | 486 | /// @notice Multiplies two unsigned 60.18-decimal fixed-point numbers together, returning a new unsigned 60.18-decimal 487 | /// fixed-point number. 488 | /// @dev See the documentation for the "PRBMath.mulDivFixedPoint" function. 489 | /// @param x The multiplicand as an unsigned 60.18-decimal fixed-point number. 490 | /// @param y The multiplier as an unsigned 60.18-decimal fixed-point number. 491 | /// @return result The result as an unsigned 60.18-decimal fixed-point number. 492 | function mul(PRBMath.UD60x18 memory x, PRBMath.UD60x18 memory y) 493 | internal 494 | pure 495 | returns (PRBMath.UD60x18 memory result) 496 | { 497 | result = PRBMath.UD60x18({ 498 | value: PRBMath.mulDivFixedPoint(x.value, y.value) 499 | }); 500 | } 501 | 502 | /// @notice Returns PI as an unsigned 60.18-decimal fixed-point number. 503 | function pi() internal pure returns (PRBMath.UD60x18 memory result) { 504 | result = PRBMath.UD60x18({value: 3141592653589793238}); 505 | } 506 | 507 | /// @notice Raises x to the power of y. 508 | /// 509 | /// @dev Based on the insight that x^y = 2^(log2(x) * y). 510 | /// 511 | /// Requirements: 512 | /// - All from "exp2", "log2" and "mul". 513 | /// 514 | /// Caveats: 515 | /// - All from "exp2", "log2" and "mul". 516 | /// - Assumes 0^0 is 1. 517 | /// 518 | /// @param x Number to raise to given power y, as an unsigned 60.18-decimal fixed-point number. 519 | /// @param y Exponent to raise x to, as an unsigned 60.18-decimal fixed-point number. 520 | /// @return result x raised to power y, as an unsigned 60.18-decimal fixed-point number. 521 | function pow(PRBMath.UD60x18 memory x, PRBMath.UD60x18 memory y) 522 | internal 523 | pure 524 | returns (PRBMath.UD60x18 memory result) 525 | { 526 | if (x.value == 0) { 527 | return PRBMath.UD60x18({value: y.value == 0 ? SCALE : uint256(0)}); 528 | } else { 529 | result = exp2(mul(log2(x), y)); 530 | } 531 | } 532 | 533 | /// @notice Raises x (unsigned 60.18-decimal fixed-point number) to the power of y (basic unsigned integer) using the 534 | /// famous algorithm "exponentiation by squaring". 535 | /// 536 | /// @dev See https://en.wikipedia.org/wiki/Exponentiation_by_squaring 537 | /// 538 | /// Requirements: 539 | /// - The result must fit within MAX_UD60x18. 540 | /// 541 | /// Caveats: 542 | /// - All from "mul". 543 | /// - Assumes 0^0 is 1. 544 | /// 545 | /// @param x The base as an unsigned 60.18-decimal fixed-point number. 546 | /// @param y The exponent as an uint256. 547 | /// @return result The result as an unsigned 60.18-decimal fixed-point number. 548 | function powu(PRBMath.UD60x18 memory x, uint256 y) 549 | internal 550 | pure 551 | returns (PRBMath.UD60x18 memory result) 552 | { 553 | // Calculate the first iteration of the loop in advance. 554 | uint256 xValue = x.value; 555 | uint256 rValue = y & 1 > 0 ? xValue : SCALE; 556 | 557 | // Equivalent to "for(y /= 2; y > 0; y /= 2)" but faster. 558 | for (y >>= 1; y > 0; y >>= 1) { 559 | xValue = PRBMath.mulDivFixedPoint(xValue, xValue); 560 | 561 | // Equivalent to "y % 2 == 1" but faster. 562 | if (y & 1 > 0) { 563 | rValue = PRBMath.mulDivFixedPoint(rValue, xValue); 564 | } 565 | } 566 | result = PRBMath.UD60x18({value: rValue}); 567 | } 568 | 569 | /// @notice Returns 1 as an unsigned 60.18-decimal fixed-point number. 570 | function scale() internal pure returns (PRBMath.UD60x18 memory result) { 571 | result = PRBMath.UD60x18({value: SCALE}); 572 | } 573 | 574 | /// @notice Calculates the square root of x, rounding down. 575 | /// @dev Uses the Babylonian method https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method. 576 | /// 577 | /// Requirements: 578 | /// - x must be less than MAX_UD60x18 / SCALE. 579 | /// 580 | /// Caveats: 581 | /// - The maximum fixed-point number permitted is 115792089237316195423570985008687907853269.984665640564039458. 582 | /// 583 | /// @param x The unsigned 60.18-decimal fixed-point number for which to calculate the square root. 584 | /// @return result The result as an unsigned 60.18-decimal fixed-point . 585 | function sqrt(PRBMath.UD60x18 memory x) 586 | internal 587 | pure 588 | returns (PRBMath.UD60x18 memory result) 589 | { 590 | require( 591 | x.value < 592 | 115792089237316195423570985008687907853269984665640564039458 593 | ); 594 | unchecked { 595 | // Multiply x by the SCALE to account for the factor of SCALE that is picked up when multiplying two unsigned 596 | // 60.18-decimal fixed-point numbers together (in this case, those two numbers are both the square root). 597 | result = PRBMath.UD60x18({value: PRBMath.sqrt(x.value * SCALE)}); 598 | } 599 | } 600 | 601 | /// @notice Subtracts one unsigned 60.18-decimal fixed-point number from another one, returning a new unsigned 60.18-decimal 602 | /// fixed-point number. 603 | /// @param x The unsigned 60.18-decimal fixed-point number to subtract from. 604 | /// @param y The unsigned 60.18-decimal fixed-point number to subtract. 605 | /// @param result The result as an unsigned 60.18 decimal fixed-point number. 606 | function sub(PRBMath.UD60x18 memory x, PRBMath.UD60x18 memory y) 607 | internal 608 | pure 609 | returns (PRBMath.UD60x18 memory result) 610 | { 611 | unchecked { 612 | require(x.value >= y.value); 613 | result = PRBMath.UD60x18({value: x.value - y.value}); 614 | } 615 | } 616 | 617 | /// @notice Converts a unsigned 60.18-decimal fixed-point number to basic integer form, rounding down in the process. 618 | /// @param x The unsigned 60.18-decimal fixed-point number to convert. 619 | /// @return result The same number in basic integer form. 620 | function toUint(PRBMath.UD60x18 memory x) 621 | internal 622 | pure 623 | returns (uint256 result) 624 | { 625 | unchecked { 626 | result = x.value / SCALE; 627 | } 628 | } 629 | } 630 | -------------------------------------------------------------------------------- /contracts/SafeTransferLib.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | pragma solidity >=0.8.0; 3 | 4 | import {ERC20} from "./ERC20.sol"; 5 | 6 | /// @notice Safe ETH and ERC20 transfer library that gracefully handles missing return values. 7 | /// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/utils/SafeTransferLib.sol) 8 | /// @author Modified from Gnosis (https://github.com/gnosis/gp-v2-contracts/blob/main/src/contracts/libraries/GPv2SafeERC20.sol) 9 | /// @dev Use with caution! Some functions in this library knowingly create dirty bits at the destination of the free memory pointer. 10 | /// @dev Note that none of the functions in this library check that a token has code at all! That responsibility is delegated to the caller. 11 | library SafeTransferLib { 12 | /*/////////////////////////////////////////////////////////////// 13 | ETH OPERATIONS 14 | //////////////////////////////////////////////////////////////*/ 15 | 16 | function safeTransferETH(address to, uint256 amount) internal { 17 | bool callStatus; 18 | 19 | assembly { 20 | // Transfer the ETH and store if it succeeded or not. 21 | callStatus := call(gas(), to, amount, 0, 0, 0, 0) 22 | } 23 | 24 | require(callStatus, "ETH_TRANSFER_FAILED"); 25 | } 26 | 27 | /*/////////////////////////////////////////////////////////////// 28 | ERC20 OPERATIONS 29 | //////////////////////////////////////////////////////////////*/ 30 | 31 | function safeTransferFrom( 32 | ERC20 token, 33 | address from, 34 | address to, 35 | uint256 amount 36 | ) internal { 37 | bool callStatus; 38 | 39 | assembly { 40 | // Get a pointer to some free memory. 41 | let freeMemoryPointer := mload(0x40) 42 | 43 | // Write the abi-encoded calldata to memory piece by piece: 44 | mstore(freeMemoryPointer, 0x23b872dd00000000000000000000000000000000000000000000000000000000) // Begin with the function selector. 45 | mstore(add(freeMemoryPointer, 4), and(from, 0xffffffffffffffffffffffffffffffffffffffff)) // Mask and append the "from" argument. 46 | mstore(add(freeMemoryPointer, 36), and(to, 0xffffffffffffffffffffffffffffffffffffffff)) // Mask and append the "to" argument. 47 | mstore(add(freeMemoryPointer, 68), amount) // Finally append the "amount" argument. No mask as it's a full 32 byte value. 48 | 49 | // Call the token and store if it succeeded or not. 50 | // We use 100 because the calldata length is 4 + 32 * 3. 51 | callStatus := call(gas(), token, 0, freeMemoryPointer, 100, 0, 0) 52 | } 53 | 54 | require(didLastOptionalReturnCallSucceed(callStatus), "TRANSFER_FROM_FAILED"); 55 | } 56 | 57 | function safeTransfer( 58 | ERC20 token, 59 | address to, 60 | uint256 amount 61 | ) internal { 62 | bool callStatus; 63 | 64 | assembly { 65 | // Get a pointer to some free memory. 66 | let freeMemoryPointer := mload(0x40) 67 | 68 | // Write the abi-encoded calldata to memory piece by piece: 69 | mstore(freeMemoryPointer, 0xa9059cbb00000000000000000000000000000000000000000000000000000000) // Begin with the function selector. 70 | mstore(add(freeMemoryPointer, 4), and(to, 0xffffffffffffffffffffffffffffffffffffffff)) // Mask and append the "to" argument. 71 | mstore(add(freeMemoryPointer, 36), amount) // Finally append the "amount" argument. No mask as it's a full 32 byte value. 72 | 73 | // Call the token and store if it succeeded or not. 74 | // We use 68 because the calldata length is 4 + 32 * 2. 75 | callStatus := call(gas(), token, 0, freeMemoryPointer, 68, 0, 0) 76 | } 77 | 78 | require(didLastOptionalReturnCallSucceed(callStatus), "TRANSFER_FAILED"); 79 | } 80 | 81 | function safeApprove( 82 | ERC20 token, 83 | address to, 84 | uint256 amount 85 | ) internal { 86 | bool callStatus; 87 | 88 | assembly { 89 | // Get a pointer to some free memory. 90 | let freeMemoryPointer := mload(0x40) 91 | 92 | // Write the abi-encoded calldata to memory piece by piece: 93 | mstore(freeMemoryPointer, 0x095ea7b300000000000000000000000000000000000000000000000000000000) // Begin with the function selector. 94 | mstore(add(freeMemoryPointer, 4), and(to, 0xffffffffffffffffffffffffffffffffffffffff)) // Mask and append the "to" argument. 95 | mstore(add(freeMemoryPointer, 36), amount) // Finally append the "amount" argument. No mask as it's a full 32 byte value. 96 | 97 | // Call the token and store if it succeeded or not. 98 | // We use 68 because the calldata length is 4 + 32 * 2. 99 | callStatus := call(gas(), token, 0, freeMemoryPointer, 68, 0, 0) 100 | } 101 | 102 | require(didLastOptionalReturnCallSucceed(callStatus), "APPROVE_FAILED"); 103 | } 104 | 105 | /*/////////////////////////////////////////////////////////////// 106 | INTERNAL HELPER LOGIC 107 | //////////////////////////////////////////////////////////////*/ 108 | 109 | function didLastOptionalReturnCallSucceed(bool callStatus) private pure returns (bool success) { 110 | assembly { 111 | // Get how many bytes the call returned. 112 | let returnDataSize := returndatasize() 113 | 114 | // If the call reverted: 115 | if iszero(callStatus) { 116 | // Copy the revert message into memory. 117 | returndatacopy(0, 0, returnDataSize) 118 | 119 | // Revert with the same message. 120 | revert(0, returnDataSize) 121 | } 122 | 123 | switch returnDataSize 124 | case 32 { 125 | // Copy the return data into memory. 126 | returndatacopy(0, 0, returnDataSize) 127 | 128 | // Set success to whether it returned true. 129 | success := iszero(iszero(mload(0))) 130 | } 131 | case 0 { 132 | // There was no return data. 133 | success := 1 134 | } 135 | default { 136 | // It returned some malformed output. 137 | success := 0 138 | } 139 | } 140 | } 141 | } -------------------------------------------------------------------------------- /contracts/interfaces/IERC20Permit.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MPL-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | interface IERC20Permit { 5 | 6 | function permit( 7 | address holder, 8 | address spender, 9 | uint256 nonce, 10 | uint256 expiry, 11 | bool allowed, 12 | uint8 v, 13 | bytes32 r, 14 | bytes32 s 15 | ) external; 16 | 17 | //EIP2612 implementation 18 | function permit( 19 | address holder, 20 | address spender, 21 | uint256 amount, 22 | uint256 expiry, 23 | uint8 v, 24 | bytes32 r, 25 | bytes32 s 26 | ) external; 27 | 28 | function nonces(address holder) external view returns(uint); 29 | 30 | function pull(address usr, uint256 wad) external; 31 | 32 | function approve(address usr, uint256 wad) external returns (bool); 33 | } -------------------------------------------------------------------------------- /contracts/interfaces/I_LearningCurve.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MPL-2.0 2 | pragma solidity 0.8.13; 3 | 4 | interface I_LearningCurve { 5 | function mintForAddress(address, uint256) external; 6 | 7 | function balanceOf(address) external view returns (uint256); 8 | } -------------------------------------------------------------------------------- /contracts/interfaces/I_Registry.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MPL-2.0 2 | pragma solidity 0.8.13; 3 | 4 | interface I_Registry { 5 | function latestVault(address) external view returns (address); 6 | } -------------------------------------------------------------------------------- /contracts/interfaces/I_Vault.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MPL-2.0 2 | pragma solidity 0.8.13; 3 | 4 | interface I_Vault { 5 | function token() external view returns (address); 6 | 7 | function underlying() external view returns (address); 8 | 9 | function pricePerShare() external view returns (uint256); 10 | 11 | function deposit(uint256) external returns (uint256); 12 | 13 | function depositAll() external; 14 | 15 | function withdraw(uint256) external returns (uint256); 16 | 17 | function withdraw() external returns (uint256); 18 | 19 | function balanceOf(address) external returns (uint256); 20 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "free-learn", 3 | "devDependencies": { 4 | "@openzeppelin/contracts": "^4.5.0", 5 | "ethlint": "^1.2.5", 6 | "husky": "^4.3.0", 7 | "prettier": "^2.3.1", 8 | "prettier-plugin-solidity": "^1.0.0-alpha.57", 9 | "pretty-quick": "^3.0.2" 10 | }, 11 | "scripts": { 12 | "lint": "pretty-quick --pattern '**/*.*(sol|json|py)' --verbose", 13 | "lint:check": "prettier --check **/*.sol **/*.json", 14 | "lint:fix": "pretty-quick --pattern '**/*.*(sol|json)' --staged --verbose", 15 | "sec:flatten": "sh ./security/flattener-run.sh" 16 | }, 17 | "dependencies": { 18 | "@commitlint/cli": "^12.1.4", 19 | "@commitlint/config-conventional": "^12.1.4" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | black==21.7b0 2 | eth-brownie==1.16.1 3 | 4 | -------------------------------------------------------------------------------- /scripts/deploy.py: -------------------------------------------------------------------------------- 1 | from brownie import * 2 | from brownie import Contract, accounts, BasicERC20 3 | 4 | 5 | def main(): 6 | deployer = accounts.load('dep') 7 | token = BasicERC20.at("0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa") 8 | lc = Contract.from_explorer("0x26A1EcDeCBeeE657e9C21273544e555F74b11d54") 9 | token.approve(lc, 1e18, {"from": deployer}) 10 | lc.initialise({"from": deployer}) -------------------------------------------------------------------------------- /scripts/flatten_contracts.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from os import path 3 | from brownie import * 4 | 5 | 6 | def _flattener(contracts_to_flatten): 7 | for contract_obj in contracts_to_flatten: 8 | contract_info = contract_obj.get_verification_info() 9 | flatten_file_name = path.join( 10 | "flattened", ".".join([contract_obj._name, "sol"]) 11 | ) 12 | with open(flatten_file_name, "w") as fl_file: 13 | fl_file.write(contract_info["flattened_source"]) 14 | 15 | 16 | def main(): 17 | contracts_to_flatten = [LearningCurve, KernelFactory] 18 | _flattener(contracts_to_flatten) 19 | -------------------------------------------------------------------------------- /security/flattener-run.sh: -------------------------------------------------------------------------------- 1 | FLAT_DIR="./flattened" 2 | 3 | if [ ! -d $FLAT_DIR ]; then mkdir $FLAT_DIR; fi 4 | 5 | rm -f ./flattened/* 6 | brownie run flatten_contracts.py $1 -------------------------------------------------------------------------------- /security/slither-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "detectors_to_exclude": "naming-convention|solc-version|external-function", 3 | "exclude_informational": false, 4 | "exclude_low": false, 5 | "exclude_medium": false, 6 | "exclude_high": false, 7 | "disable_color": false, 8 | "filter_paths": "test|OpenZeppelin|iearn-finance", 9 | "solc": "0.8.4" 10 | } 11 | -------------------------------------------------------------------------------- /tests-mainnet/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import time 3 | import constants_mainnet 4 | from brownie import ( 5 | DeSchool, 6 | LearningCurve, 7 | accounts, 8 | web3, 9 | Wei, 10 | chain, 11 | Contract, 12 | ) 13 | 14 | 15 | @pytest.fixture(scope="function", autouse=True) 16 | def isolate_func(fn_isolation): 17 | # perform a chain rewind after completing each test, to ensure proper isolation 18 | # https://eth-brownie.readthedocs.io/en/v1.10.3/tests-pytest-intro.html#isolation-fixtures 19 | pass 20 | 21 | 22 | @pytest.fixture 23 | def deployer(): 24 | yield accounts.at("0x075e72a5eDf65F0A5f44699c7654C1a76941Ddc8", force=True) 25 | 26 | 27 | @pytest.fixture(scope="function", autouse=True) 28 | def contracts(deployer, token): 29 | learning_curve = LearningCurve.deploy(token.address, {"from": deployer}) 30 | token.transfer(deployer, 1e18, {"from": deployer}) 31 | token.approve(learning_curve, 1e18, {"from": deployer}) 32 | learning_curve.initialise({"from": deployer}) 33 | yield DeSchool.deploy( 34 | token.address, 35 | learning_curve.address, 36 | constants_mainnet.REGISTRY, 37 | {"from": deployer}), \ 38 | learning_curve 39 | 40 | 41 | @pytest.fixture(scope="function") 42 | def contracts_with_courses(contracts, steward): 43 | deschool, learning_curve = contracts 44 | for n in range(5): 45 | tx = deschool.createCourse( 46 | constants_mainnet.STAKE, 47 | constants_mainnet.DURATION, 48 | constants_mainnet.URL, 49 | steward, 50 | {"from": steward} 51 | ) 52 | yield deschool, learning_curve 53 | 54 | 55 | @pytest.fixture(scope="function") 56 | def contracts_with_scholarships(contracts_with_courses, token, deployer, provider): 57 | deschool, learning_curve = contracts_with_courses 58 | token.transfer(provider, (constants_mainnet.SCHOLARSHIP_AMOUNT * 5), {"from": deployer}) 59 | assert token.balanceOf(provider) == (constants_mainnet.SCHOLARSHIP_AMOUNT * 5) 60 | token.approve(deschool, (constants_mainnet.SCHOLARSHIP_AMOUNT * 5), {"from": provider}) 61 | for n in range(5): 62 | tx = deschool.createScholarships( 63 | n, 64 | constants_mainnet.SCHOLARSHIP_AMOUNT, 65 | {"from": provider} 66 | ) 67 | yield deschool, learning_curve 68 | 69 | @pytest.fixture(scope="function") 70 | def contracts_with_learners(contracts_with_courses, learners, token, deployer): 71 | deschool, learning_curve = contracts_with_courses 72 | for n, learner in enumerate(learners): 73 | token.transfer(learner, constants_mainnet.STAKE, {"from": deployer}) 74 | token.approve(deschool, constants_mainnet.STAKE, {"from": learner}) 75 | deschool.register(0, {"from": learner}) 76 | yield deschool, learning_curve 77 | 78 | 79 | @pytest.fixture 80 | def steward(accounts): 81 | yield accounts[1] 82 | 83 | 84 | @pytest.fixture 85 | def learners(accounts): 86 | yield accounts[2:6] 87 | 88 | 89 | @pytest.fixture 90 | def provider(accounts): 91 | yield accounts[7] 92 | 93 | 94 | @pytest.fixture 95 | def hackerman(accounts): 96 | yield accounts[8] 97 | 98 | 99 | @pytest.fixture 100 | def token(): 101 | yield Contract.from_explorer(constants_mainnet.DAI) 102 | 103 | 104 | @pytest.fixture 105 | def ytoken(): 106 | yield Contract.from_explorer(constants_mainnet.VAULT) 107 | 108 | 109 | @pytest.fixture 110 | def gen_lev_strat(): 111 | yield Contract.from_explorer(constants_mainnet.GEN_LEV) 112 | 113 | 114 | @pytest.fixture 115 | def keeper(): 116 | yield accounts.at(constants_mainnet.KEEPER, force=True) 117 | 118 | 119 | @pytest.fixture 120 | def kernelTreasury(accounts): 121 | yield accounts.at("0x297a3C4B8bB87E671d31C475C5DbE434E24dFC1F", force=True) 122 | 123 | -------------------------------------------------------------------------------- /tests-mainnet/constants_mainnet.py: -------------------------------------------------------------------------------- 1 | DAI = "0x6B175474E89094C44Da98b954EedeAC495271d0F" 2 | VAULT = "0xdA816459F1AB5631232FE5e97a05BBBb94970c95" 3 | REGISTRY = "0x50c1a2eA0a861A967D9d0FFE2AE4012c2E053804" 4 | KEEPER= "0x736d7e3c5a6cb2ce3b764300140abf476f6cfccf" 5 | GEN_LEV = "0x1676055fE954EE6fc388F9096210E5EbE0A9070c" 6 | STAKE = 1_000e18 7 | SCHOLARSHIP_AMOUNT = 2e21 8 | SCHOLARSHIP_WITHDRAWAL = 1e21 9 | DURATION = 10000 10 | COURSE_RUNNING = 50 11 | URL = "https://www.kernel.community" 12 | CREATOR = "0x297a3C4B8bB87E671d31C475C5DbE434E24dFC1F" 13 | 14 | MINT_AMOUNT = 1_000e18 15 | K = 10_000 16 | ACCURACY = 1e8 17 | ACCURACY_Y = 1e9 18 | -------------------------------------------------------------------------------- /tests-mainnet/test_mainnet_ds.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import constants_mainnet 3 | from eth_account import Account 4 | from eth_account._utils.structured_data.hashing import hash_domain 5 | from eth_account.messages import encode_structured_data 6 | from eth_utils import encode_hex 7 | 8 | def test_redeem(contracts_with_learners, learners, token, steward, keeper, gen_lev_strat, ytoken): 9 | deschool, learning_curve = contracts_with_learners 10 | brownie.chain.mine(constants_mainnet.COURSE_RUNNING) 11 | tx = deschool.batchDeposit({"from": keeper}) 12 | brownie.chain.mine(constants_mainnet.DURATION) 13 | brownie.chain.sleep(1000) 14 | gen_lev_strat.harvest({"from": keeper}) 15 | for n, learner in enumerate(learners): 16 | learner_before = token.balanceOf(learner) 17 | ds_ytoken_balance = ytoken.balanceOf(deschool) 18 | tx = deschool.redeem(0, {"from": learner}) 19 | learner_after = token.balanceOf(learner) 20 | assert "StakeRedeemed" in tx.events 21 | assert tx.events["StakeRedeemed"]["amount"] == learner_after - learner_before 22 | assert tx.events["StakeRedeemed"]["amount"] <= constants_mainnet.STAKE 23 | assert deschool.verify(learner, 0) 24 | assert token.balanceOf(steward) == 0 25 | assert deschool.getYieldRewards(steward.address, {"from": steward}) > 0 26 | assert ytoken.balanceOf(deschool) < ds_ytoken_balance 27 | assert token.balanceOf(learning_curve) == 1e18 28 | assert ytoken.balanceOf(learning_curve) == 0 29 | kt_redemption = deschool.getYieldRewards(steward.address, {"from": steward}) 30 | deschool.withdrawYieldRewards({"from": steward}) 31 | assert deschool.getYieldRewards(steward.address, {"from": steward}) == 0 32 | assert token.balanceOf(steward) == kt_redemption 33 | assert token.balanceOf(deschool) == 0 34 | assert ytoken.balanceOf(deschool) < 1000 35 | 36 | def test_mint(contracts_with_learners, learners, token, keeper, gen_lev_strat, ytoken, steward): 37 | deschool, learning_curve = contracts_with_learners 38 | brownie.chain.mine(constants_mainnet.COURSE_RUNNING) 39 | tx = deschool.batchDeposit({"from": keeper}) 40 | brownie.chain.mine(constants_mainnet.DURATION) 41 | brownie.chain.sleep(1000) 42 | gen_lev_strat.harvest({"from": keeper}) 43 | for n, learner in enumerate(learners): 44 | mintable_balance = learning_curve.getMintableForReserveAmount(constants_mainnet.STAKE) 45 | lc_token_balance = token.balanceOf(learning_curve) 46 | ds_token_balance = token.balanceOf(deschool) 47 | learner_lc_balance = learning_curve.balanceOf(learner) 48 | tx = deschool.mint(0, {"from": learner}) 49 | assert "LearnMintedFromCourse" in tx.events 50 | assert abs(tx.events["LearnMintedFromCourse"]["learnMinted"] - mintable_balance) < constants_mainnet.ACCURACY_Y 51 | assert abs(tx.events["LearnMintedFromCourse"]["stableConverted"] - constants_mainnet.STAKE) < constants_mainnet.ACCURACY_Y 52 | assert deschool.verify(learner, 0) 53 | assert abs(token.balanceOf(learning_curve) - lc_token_balance - constants_mainnet.STAKE) < constants_mainnet.ACCURACY_Y 54 | assert abs(token.balanceOf(deschool) - deschool.getYieldRewards(steward.address)) < constants_mainnet.ACCURACY 55 | assert abs(learning_curve.balanceOf(learner) - learner_lc_balance - mintable_balance) < constants_mainnet.ACCURACY_Y 56 | assert token.balanceOf(deschool) == deschool.getYieldRewards(steward.address) 57 | assert ytoken.balanceOf(deschool) < 1000 58 | 59 | def test_batch_success( 60 | contracts_with_learners, 61 | keeper, 62 | token, 63 | ytoken, 64 | learners 65 | ): 66 | deschool, learning_curve = contracts_with_learners 67 | ds_balance_before = token.balanceOf(deschool) 68 | batch_id_before = deschool.getCurrentBatchId() 69 | tx = deschool.batchDeposit({"from": keeper}) 70 | assert "BatchDeposited" in tx.events 71 | assert batch_id_before == \ 72 | 0 == \ 73 | tx.events["BatchDeposited"]["batchId"] == \ 74 | deschool.getCurrentBatchId() - 1 75 | assert token.balanceOf(deschool) == 0 76 | assert ds_balance_before == tx.events["BatchDeposited"]["batchAmount"] == constants_mainnet.STAKE * len(learners) 77 | assert ytoken.balanceOf(deschool) > 0 78 | assert ytoken.balanceOf(deschool) == tx.events["BatchDeposited"]["batchYieldAmount"] 79 | 80 | 81 | def test_batch_malicious(contracts_with_courses, keeper): 82 | deschool, learning_curve = contracts_with_courses 83 | with brownie.reverts("batchDeposit: no funds to deposit"): 84 | deschool.batchDeposit({"from": keeper}) 85 | 86 | 87 | def test_register_diff_batches(contracts_with_courses, keeper, token, learners, deployer, ytoken): 88 | deschool, learning_curve = contracts_with_courses 89 | for n, learner in enumerate(learners): 90 | token.transfer( 91 | learner, 92 | constants_mainnet.STAKE, 93 | {"from": deployer} 94 | ) 95 | token.approve(deschool, constants_mainnet.STAKE, {"from": learner}) 96 | before_bal = token.balanceOf(deschool) 97 | tx = deschool.register(0, {"from": learner}) 98 | 99 | assert "LearnerRegistered" in tx.events 100 | assert tx.events["LearnerRegistered"]["courseId"] == 0 101 | assert before_bal + constants_mainnet.STAKE == token.balanceOf(deschool) 102 | ds_balance_before = token.balanceOf(deschool) 103 | batch_id_before = deschool.getCurrentBatchId() 104 | ytoken_bal_before = ytoken.balanceOf(deschool) 105 | tx = deschool.batchDeposit({"from": keeper}) 106 | assert "BatchDeposited" in tx.events 107 | assert batch_id_before == \ 108 | n == \ 109 | tx.events["BatchDeposited"]["batchId"] == \ 110 | deschool.getCurrentBatchId() - 1 111 | assert token.balanceOf(deschool) == 0 112 | assert ds_balance_before == tx.events["BatchDeposited"]["batchAmount"] == constants_mainnet.STAKE 113 | assert ytoken.balanceOf(deschool) > ytoken_bal_before 114 | assert ytoken.balanceOf(deschool) - ytoken_bal_before == tx.events["BatchDeposited"]["batchYieldAmount"] 115 | 116 | 117 | def test_verify(contracts_with_learners, learners, keeper): 118 | deschool, learning_curve = contracts_with_learners 119 | learner = learners[0] 120 | tx = deschool.batchDeposit({"from": keeper}) 121 | assert not(deschool.verify(learner, 0, {"from": learner})) 122 | brownie.chain.mine(constants_mainnet.DURATION) 123 | assert deschool.verify(learner, 0, {"from": learner}) 124 | 125 | 126 | def build_permit(holder, spender, token, expiry): 127 | data = { 128 | "types": { 129 | "EIP712Domain": [ 130 | {"name": "name", "type": "string"}, 131 | {"name": "version", "type": "string"}, 132 | {"name": "chainId", "type": "uint256"}, 133 | {"name": "verifyingContract", "type": "address"}, 134 | ], 135 | "Permit": [ 136 | {"name": "holder", "type": "address"}, 137 | {"name": "spender", "type": "address"}, 138 | {"name": "nonce", "type": "uint256"}, 139 | {"name": "expiry", "type": "uint256"}, 140 | {"name": "allowed", "type": "bool"}, 141 | ], 142 | }, 143 | "domain": { 144 | "name": token.name(), 145 | "version": token.version(), 146 | "chainId": 1, 147 | "verifyingContract": str(token), 148 | }, 149 | "primaryType": "Permit", 150 | "message": { 151 | "holder": holder, 152 | "spender": spender, 153 | "nonce": token.nonces(holder), 154 | "expiry": 0, 155 | "allowed": True, 156 | }, 157 | } 158 | assert encode_hex(hash_domain(data)) == token.DOMAIN_SEPARATOR() 159 | return encode_structured_data(data) 160 | 161 | 162 | def test_create_scholarships(contracts_with_scholarships, token, deployer): 163 | deschool, learning_curve = contracts_with_scholarships 164 | # provide another scholarship, from a separate account, to the first course 165 | token.approve(deschool, (constants_mainnet.SCHOLARSHIP_AMOUNT), {"from": deployer}) 166 | tx = deschool.createScholarships( 167 | 0, 168 | constants_mainnet.SCHOLARSHIP_AMOUNT, 169 | {"from": deployer} 170 | ) 171 | assert "ScholarshipCreated" in tx.events 172 | assert tx.events["ScholarshipCreated"]["courseId"] == 0 173 | assert tx.events["ScholarshipCreated"]["newScholars"] == (constants_mainnet.SCHOLARSHIP_AMOUNT / constants_mainnet.STAKE) 174 | assert tx.events["ScholarshipCreated"]["scholarshipTotal"] == (constants_mainnet.SCHOLARSHIP_AMOUNT * 2) 175 | assert tx.events["ScholarshipCreated"]["scholarshipProvider"] == deployer 176 | 177 | 178 | def test_scholarship_reverts(contracts_with_scholarships, token, deployer, provider): 179 | deschool, learning_curve = contracts_with_scholarships 180 | token.transfer(provider, (constants_mainnet.SCHOLARSHIP_AMOUNT), {"from": deployer}) 181 | assert token.balanceOf(provider) == (constants_mainnet.SCHOLARSHIP_AMOUNT) 182 | token.approve(deschool, (constants_mainnet.SCHOLARSHIP_AMOUNT), {"from": provider}) 183 | with brownie.reverts("createScholarships: must seed scholarship with enough funds to justify gas costs"): 184 | deschool.createScholarships( 185 | 0, 186 | constants_mainnet.SCHOLARSHIP_AMOUNT / 3, 187 | {"from": provider} 188 | ) 189 | with brownie.reverts("createScholarships: courseId does not exist"): 190 | deschool.createScholarships( 191 | 6, 192 | constants_mainnet.SCHOLARSHIP_AMOUNT, 193 | {"from": provider} 194 | ) 195 | 196 | 197 | def test_permit_create_scholarships(contracts_with_courses, learners, token, deployer): 198 | deschool, learning_curve = contracts_with_courses 199 | signer = Account.create() 200 | holder = signer.address 201 | token.transfer(holder, constants_mainnet.SCHOLARSHIP_AMOUNT, {"from": deployer}) 202 | assert token.balanceOf(holder) == constants_mainnet.SCHOLARSHIP_AMOUNT 203 | permit = build_permit(holder, str(deschool), token, 3600) 204 | signed = signer.sign_message(permit) 205 | print(token.balanceOf(deschool.address)) 206 | tx = deschool.permitCreateScholarships(0, constants_mainnet.SCHOLARSHIP_AMOUNT, 0, 0, signed.v, signed.r, signed.s, {"from": holder}) 207 | print(token.balanceOf(deschool.address)) 208 | assert "ScholarshipCreated" in tx.events 209 | assert tx.events["ScholarshipCreated"]["courseId"] == 0 210 | assert tx.events["ScholarshipCreated"]["scholarshipAmount"] == constants_mainnet.SCHOLARSHIP_AMOUNT 211 | 212 | 213 | def test_register_scholar(contracts_with_scholarships, learners): 214 | deschool, learning_curve = contracts_with_scholarships 215 | for n, learner in enumerate(learners): 216 | tx = deschool.registerScholar( 217 | n, 218 | {"from": learner} 219 | ) 220 | assert "ScholarRegistered" in tx.events 221 | assert tx.events["ScholarRegistered"]["courseId"] == n 222 | assert tx.events["ScholarRegistered"]["scholar"] == learner 223 | tx = deschool.registerScholar( 224 | 0, 225 | {"from": learners[1]} 226 | ) 227 | assert not(deschool.scholarshipAvailable(0)) 228 | with brownie.reverts("registerScholar: no scholarships available for this course"): 229 | tx = deschool.registerScholar( 230 | 0, 231 | {"from": learners[2]} 232 | ) 233 | brownie.chain.mine(constants_mainnet.DURATION) 234 | # this should now succeed 235 | assert deschool.scholarshipAvailable(0) 236 | tx = deschool.registerScholar( 237 | 0, 238 | {"from": learners[2]} 239 | ) 240 | assert "ScholarRegistered" in tx.events 241 | assert tx.events["ScholarRegistered"]["courseId"] == 0 242 | # check that scholars is increased to 3 243 | assert deschool.courses(0)[4] == 3 244 | # check that completedScholars is set to 1 245 | assert deschool.courses(0)[5] == 1 246 | 247 | 248 | def test_register_scholar_reverts(contracts_with_scholarships, learners): 249 | deschool, learning_curve = contracts_with_scholarships 250 | with brownie.reverts("registerScholar: courseId does not exist"): 251 | deschool.registerScholar( 252 | 6, 253 | {"from": learners[0]} 254 | ) 255 | tx = deschool.registerScholar( 256 | 0, 257 | {"from": learners[0]} 258 | ) 259 | with brownie.reverts("registerScholar: already registered"): 260 | tx = deschool.registerScholar( 261 | 0, 262 | {"from": learners[0]} 263 | ) 264 | 265 | 266 | def test_withdraw_scholarships(contracts_with_scholarships, token, provider): 267 | deschool, learning_curve = contracts_with_scholarships 268 | prov_bal_before = token.balanceOf(provider) 269 | tx = deschool.withdrawScholarship( 270 | 0, 271 | constants_mainnet.SCHOLARSHIP_AMOUNT, 272 | {"from": provider} 273 | ) 274 | assert "ScholarshipWithdrawn" in tx.events 275 | assert tx.events["ScholarshipWithdrawn"]["courseId"] == 0 276 | assert tx.events["ScholarshipWithdrawn"]["amountWithdrawn"] == constants_mainnet.SCHOLARSHIP_AMOUNT 277 | # the -1 is because of a rounding error in the mul div operation we do to ensure we cater for the case that the vault makes a loss 278 | assert token.balanceOf(provider) == prov_bal_before + constants_mainnet.SCHOLARSHIP_AMOUNT - 1 279 | # we can check the scholarshipTotal slot in the course struct to be sure 280 | assert deschool.courses(0)[5] == 0 281 | # Now try and withdraw only half the amount initially provided for another course 282 | tx = deschool.withdrawScholarship( 283 | 1, 284 | constants_mainnet.SCHOLARSHIP_AMOUNT / 2, 285 | {"from": provider} 286 | ) 287 | assert "ScholarshipWithdrawn" in tx.events 288 | assert tx.events["ScholarshipWithdrawn"]["courseId"] == 1 289 | assert tx.events["ScholarshipWithdrawn"]["amountWithdrawn"] == constants_mainnet.SCHOLARSHIP_AMOUNT / 2 290 | assert deschool.courses(1)[6] == tx.events["ScholarshipWithdrawn"]["amountWithdrawn"] 291 | # this rounding error depends on the amount and it's relation to the total, it seems 292 | assert token.balanceOf(provider) == prov_bal_before + constants_mainnet.SCHOLARSHIP_AMOUNT + constants_mainnet.SCHOLARSHIP_AMOUNT / 2 - 2 293 | 294 | 295 | def test_withdraw_scholarships_reverts(contracts_with_scholarships, provider, hackerman): 296 | deschool, learning_curve = contracts_with_scholarships 297 | with brownie.reverts("withdrawScholarship: can only withdraw up to the amount initally provided for scholarships"): 298 | deschool.withdrawScholarship( 299 | 0, 300 | constants_mainnet.SCHOLARSHIP_AMOUNT * 2, 301 | {"from": provider} 302 | ) 303 | with brownie.reverts("withdrawScholarship: can only withdraw up to the amount initally provided for scholarships"): 304 | deschool.withdrawScholarship( 305 | 0, 306 | constants_mainnet.SCHOLARSHIP_AMOUNT, 307 | {"from": hackerman} 308 | ) 309 | 310 | 311 | def test_register_permit(contracts_with_courses, learners, token, deployer): 312 | deschool, learning_curve = contracts_with_courses 313 | signer = Account.create() 314 | holder = signer.address 315 | token.transfer(holder, constants_mainnet.STAKE, {"from": deployer}) 316 | assert token.balanceOf(holder) == constants_mainnet.STAKE 317 | permit = build_permit(holder, str(deschool), token, 3600) 318 | signed = signer.sign_message(permit) 319 | print(token.balanceOf(deschool.address)) 320 | tx = deschool.permitAndRegister(0, 0, 0, signed.v, signed.r, signed.s, {"from": holder}) 321 | print(token.balanceOf(deschool.address)) 322 | assert "LearnerRegistered" in tx.events 323 | assert tx.events["LearnerRegistered"]["courseId"] == 0 324 | 325 | -------------------------------------------------------------------------------- /tests-mainnet/test_operation_mainnet.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import constants_mainnet 3 | 4 | def test_full_redeem( 5 | deployer, 6 | learners, 7 | steward, 8 | contracts, 9 | kernelTreasury, 10 | keeper, 11 | token, 12 | ytoken, 13 | gen_lev_strat 14 | ): 15 | deschool, learning_curve = contracts 16 | 17 | tx = deschool.createCourse( 18 | constants_mainnet.STAKE, 19 | constants_mainnet.DURATION, 20 | constants_mainnet.URL, 21 | constants_mainnet.CREATOR, 22 | {"from": steward} 23 | ) 24 | 25 | assert "CourseCreated" in tx.events 26 | assert tx.events["CourseCreated"]["courseId"] == 0 27 | assert tx.events["CourseCreated"]["duration"] == constants_mainnet.DURATION 28 | assert tx.events["CourseCreated"]["stake"] == constants_mainnet.STAKE 29 | assert tx.events["CourseCreated"]["url"] == constants_mainnet.URL 30 | assert tx.events["CourseCreated"]["creator"] == constants_mainnet.CREATOR 31 | assert deschool.getNextCourseId() == 1 32 | 33 | for n, learner in enumerate(learners): 34 | token.transfer( 35 | learner, 36 | constants_mainnet.STAKE, 37 | {"from": deployer} 38 | ) 39 | token.approve(deschool, constants_mainnet.STAKE, {"from": learner}) 40 | before_bal = token.balanceOf(deschool) 41 | tx = deschool.register(0, {"from": learner}) 42 | 43 | assert "LearnerRegistered" in tx.events 44 | assert tx.events["LearnerRegistered"]["courseId"] == 0 45 | assert before_bal + constants_mainnet.STAKE == token.balanceOf(deschool) 46 | 47 | assert deschool.getCurrentBatchTotal() == constants_mainnet.STAKE * len(learners) 48 | assert token.balanceOf(deschool) == constants_mainnet.STAKE * len(learners) 49 | tx = deschool.batchDeposit({"from": kernelTreasury}) 50 | assert token.balanceOf(deschool) == 0 51 | assert deschool.getCurrentBatchId() == 1 52 | assert ytoken.balanceOf(deschool) > 0 53 | brownie.chain.mine(constants_mainnet.COURSE_RUNNING) 54 | gen_lev_strat.harvest({"from": keeper}) 55 | brownie.chain.mine(constants_mainnet.DURATION) 56 | 57 | assert deschool.verify(learners[0], 0, {"from": steward}) 58 | print("----- REDEEM -----") 59 | for n, learner in enumerate(learners): 60 | tx = deschool.redeem(0, {"from": learner}) 61 | print("Learner " + str(n) + " balance: " + str(learning_curve.balanceOf(learner))) 62 | print("Learner " + str(n) + " token balance: " + str(token.balanceOf(learner))) 63 | print("YDAI Balance: " + str(ytoken.balanceOf(deschool))) 64 | print("redeemable DAI Balance of Creator: " + 65 | str(deschool.getYieldRewards(steward, {"from": kernelTreasury})) 66 | ) 67 | print("DAI collateral: " + str(token.balanceOf(learning_curve))) 68 | print("Total Supply: " + str(learning_curve.totalSupply())) 69 | print('\n') 70 | 71 | 72 | def test_full_mint( 73 | deployer, 74 | learners, 75 | steward, 76 | contracts, 77 | kernelTreasury, 78 | keeper, 79 | token, 80 | ytoken, 81 | gen_lev_strat 82 | ): 83 | deschool, learning_curve = contracts 84 | 85 | tx = deschool.createCourse( 86 | constants_mainnet.STAKE, 87 | constants_mainnet.DURATION, 88 | constants_mainnet.URL, 89 | constants_mainnet.CREATOR, 90 | {"from": steward} 91 | ) 92 | 93 | assert "CourseCreated" in tx.events 94 | assert tx.events["CourseCreated"]["courseId"] == 0 95 | assert tx.events["CourseCreated"]["stake"] == constants_mainnet.STAKE 96 | assert tx.events["CourseCreated"]["duration"] == constants_mainnet.DURATION 97 | assert tx.events["CourseCreated"]["url"] == constants_mainnet.URL 98 | assert tx.events["CourseCreated"]["creator"] == constants_mainnet.CREATOR 99 | assert deschool.getNextCourseId() == 1 100 | 101 | for n, learner in enumerate(learners): 102 | token.transfer( 103 | learner, 104 | constants_mainnet.STAKE, 105 | {"from": deployer} 106 | ) 107 | token.approve(deschool, constants_mainnet.STAKE, {"from": learner}) 108 | before_bal = token.balanceOf(deschool) 109 | tx = deschool.register(0, {"from": learner}) 110 | 111 | assert "LearnerRegistered" in tx.events 112 | assert tx.events["LearnerRegistered"]["courseId"] == 0 113 | assert before_bal + constants_mainnet.STAKE == token.balanceOf(deschool) 114 | 115 | assert deschool.getCurrentBatchTotal() == constants_mainnet.STAKE * len(learners) 116 | assert token.balanceOf(deschool) == constants_mainnet.STAKE * len(learners) 117 | tx = deschool.batchDeposit({"from": kernelTreasury}) 118 | brownie.chain.mine(constants_mainnet.DURATION) 119 | gen_lev_strat.harvest({"from": keeper}) 120 | assert token.balanceOf(deschool) == 0 121 | assert deschool.getCurrentBatchId() == 1 122 | assert ytoken.balanceOf(deschool) > 0 123 | assert deschool.verify(learners[0], 0, {"from": steward}) 124 | print("----- MINT -----") 125 | for n, learner in enumerate(learners): 126 | tx = deschool.mint(0, {"from": learner}) 127 | assert "LearnMintedFromCourse" in tx.events 128 | print("Learner " + str(n) + " balance: " + str(learning_curve.balanceOf(learner))) 129 | print("YDAI Balance: " + str(ytoken.balanceOf(deschool))) 130 | print("DAI collateral: " + str(token.balanceOf(learning_curve))) 131 | print("Total Supply: " + str(learning_curve.totalSupply())) 132 | print('\n') 133 | 134 | n = 0 135 | print("----- BURN -----") 136 | for learner in reversed(learners): 137 | lc_balance_before = learning_curve.balanceOf(learner) 138 | print("Learner " + str(n) + " balance before: " + str(lc_balance_before)) 139 | learning_curve.approve( 140 | learning_curve, 141 | lc_balance_before, 142 | {"from": learner}) 143 | tx = learning_curve.burn(lc_balance_before, {"from": learner}) 144 | assert learning_curve.balanceOf(learner) < lc_balance_before 145 | assert token.balanceOf(learner) - constants_mainnet.STAKE < constants_mainnet.ACCURACY 146 | print("Learner " + str(n) + " balance: " + str(learning_curve.balanceOf(learner))) 147 | print("DAI balance: " + str(token.balanceOf(learner))) 148 | print("DAI collateral: " + str(token.balanceOf(learning_curve))) 149 | print("Total Supply: " + str(learning_curve.totalSupply())) 150 | print('\n') 151 | n += 1 152 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import constants_unit 3 | from brownie import ( 4 | DeSchool, 5 | LearningCurve, 6 | Dai, 7 | ) 8 | 9 | 10 | @pytest.fixture(scope="function", autouse=True) 11 | def isolate_func(fn_isolation): 12 | # perform a chain rewind after completing each test, to ensure proper isolation 13 | # https://eth-brownie.readthedocs.io/en/v1.10.3/tests-pytest-intro.html#isolation-fixtures 14 | pass 15 | 16 | 17 | @pytest.fixture(scope="function", autouse=True) 18 | def token(deployer): 19 | token = Dai.deploy(1, {"from": deployer}) 20 | token.mint(deployer, 1_000_000_000_000_000_000e18) 21 | yield token 22 | 23 | 24 | @pytest.fixture(scope="function") 25 | def contracts(deployer, token): 26 | learning_curve = LearningCurve.deploy(token.address, {"from": deployer}) 27 | token.approve(learning_curve, 1e18, {"from": deployer}) 28 | learning_curve.initialise({"from": deployer}) 29 | yield DeSchool.deploy( 30 | token.address, 31 | learning_curve.address, 32 | constants_unit.REGISTRY, 33 | {"from": deployer}), \ 34 | learning_curve 35 | 36 | 37 | @pytest.fixture(scope="function") 38 | def contracts_with_courses(contracts, steward): 39 | deschool, learning_curve = contracts 40 | for n in range(5): 41 | tx = deschool.createCourse( 42 | constants_unit.STAKE, 43 | constants_unit.DURATION, 44 | constants_unit.URL, 45 | constants_unit.CREATOR, 46 | {"from": steward} 47 | ) 48 | yield deschool, learning_curve 49 | 50 | 51 | @pytest.fixture(scope="function") 52 | def contracts_with_learners(contracts_with_courses, learners, token, deployer): 53 | deschool, learning_curve = contracts_with_courses 54 | for n, learner in enumerate(learners): 55 | token.transfer(learner, constants_unit.STAKE, {"from": deployer}) 56 | token.approve(deschool, constants_unit.STAKE, {"from": learner}) 57 | deschool.register(0, {"from": learner}) 58 | yield deschool, learning_curve 59 | 60 | 61 | @pytest.fixture 62 | def deployer(accounts): 63 | yield accounts[0] 64 | 65 | 66 | @pytest.fixture 67 | def steward(accounts): 68 | yield accounts[1] 69 | 70 | 71 | @pytest.fixture 72 | def hackerman(accounts): 73 | yield accounts[9] 74 | 75 | 76 | @pytest.fixture 77 | def learners(accounts): 78 | yield accounts[2:8] 79 | 80 | 81 | @pytest.fixture 82 | def kernelTreasury(accounts): 83 | yield accounts.at("0x297a3C4B8bB87E671d31C475C5DbE434E24dFC1F", force=True) 84 | 85 | -------------------------------------------------------------------------------- /tests/constants_unit.py: -------------------------------------------------------------------------------- 1 | DAI = "0x6B175474E89094C44Da98b954EedeAC495271d0F" 2 | VAULT = "0xdA816459F1AB5631232FE5e97a05BBBb94970c95" 3 | REGISTRY = "0x50c1a2eA0a861A967D9d0FFE2AE4012c2E053804" 4 | STAKE = 1e18 5 | DURATION = 500 6 | COURSE_RUNNING = 50 7 | URL = "https://www.kernel.community" 8 | CREATOR = "0x297a3C4B8bB87E671d31C475C5DbE434E24dFC1F" 9 | 10 | MALICIOUS_AMOUNT = 1_000_000_000_000_000e18 11 | MINT_AMOUNT = 10_000e18 12 | K = 10_000 13 | ACCURACY = 1e8 14 | -------------------------------------------------------------------------------- /tests/test_operation_unit.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import constants_unit 3 | 4 | 5 | def test_full(deployer, learners, steward, contracts, token): 6 | deschool, learning_curve = contracts 7 | 8 | tx = deschool.createCourse( 9 | constants_unit.STAKE, 10 | constants_unit.DURATION, 11 | constants_unit.URL, 12 | constants_unit.CREATOR, 13 | {"from": steward} 14 | ) 15 | 16 | assert "CourseCreated" in tx.events 17 | assert tx.events["CourseCreated"]["courseId"] == 0 18 | assert tx.events["CourseCreated"]["stake"] == constants_unit.STAKE 19 | assert tx.events["CourseCreated"]["duration"] == constants_unit.DURATION 20 | assert tx.events["CourseCreated"]["url"] == constants_unit.URL 21 | assert tx.events["CourseCreated"]["creator"] == constants_unit.CREATOR 22 | 23 | for n, learner in enumerate(learners): 24 | token.transfer( 25 | learner, 26 | constants_unit.STAKE, 27 | {"from": deployer} 28 | ) 29 | token.approve(deschool, constants_unit.STAKE, {"from": learner}) 30 | before_bal = token.balanceOf(deschool) 31 | tx = deschool.register(0, {"from": learner}) 32 | 33 | assert "LearnerRegistered" in tx.events 34 | assert tx.events["LearnerRegistered"]["courseId"] == 0 35 | assert before_bal + constants_unit.STAKE == token.balanceOf(deschool) 36 | 37 | assert deschool.getCurrentBatchTotal() == constants_unit.STAKE * len(learners) 38 | assert token.balanceOf(deschool) == constants_unit.STAKE * len(learners) 39 | 40 | brownie.chain.mine(constants_unit.DURATION) 41 | 42 | assert deschool.verify(learners[0], 0, {"from": steward}) 43 | 44 | for n, learner in enumerate(learners): 45 | tx = deschool.mint(0, {"from": learner}) 46 | assert "LearnMintedFromCourse" in tx.events 47 | print("Learner " + str(n) + " balance: " + str(learning_curve.balanceOf(learner))) 48 | print("DAI collateral: " + str(token.balanceOf(learning_curve))) 49 | print("Total Supply: " + str(learning_curve.totalSupply())) 50 | print('\n') 51 | n = 0 52 | 53 | for learner in reversed(learners): 54 | 55 | print("Learner " + str(n) + " balance before: " + str(learning_curve.balanceOf(learner))) 56 | learning_curve.approve( 57 | learning_curve, 58 | learning_curve.balanceOf(learner), 59 | {"from": learner}) 60 | tx = learning_curve.burn(learning_curve.balanceOf(learner), {"from": learner}) 61 | print("Learner " + str(n) + " balance: " + str(learning_curve.balanceOf(learner))) 62 | print("DAI balance: " + str(token.balanceOf(learner))) 63 | print("DAI collateral: " + str(token.balanceOf(learning_curve))) 64 | print("Total Supply: " + str(learning_curve.totalSupply())) 65 | print('\n') 66 | n += 1 -------------------------------------------------------------------------------- /tests/test_unit_ds.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | from brownie import LearningCurve, DeSchool 3 | import constants_unit 4 | 5 | from eth_account import Account 6 | from eth_account._utils.structured_data.hashing import hash_domain 7 | from eth_account.messages import encode_structured_data 8 | from eth_utils import encode_hex 9 | 10 | 11 | def test_register_permit(contracts_with_courses, learners, token, deployer): 12 | deschool, learning_curve = contracts_with_courses 13 | signer = Account.create() 14 | holder = signer.address 15 | token.transfer(holder, constants_unit.STAKE, {"from": deployer}) 16 | assert token.balanceOf(holder) == constants_unit.STAKE 17 | permit = build_permit(holder, str(deschool), token, 3600) 18 | signed = signer.sign_message(permit) 19 | print(token.balanceOf(deschool.address)) 20 | tx = deschool.permitAndRegister(0, 0, 0, signed.v, signed.r, signed.s, {"from": holder}) 21 | print(token.balanceOf(deschool.address)) 22 | print(deschool.getYieldRewards(deployer.address)) 23 | assert "LearnerRegistered" in tx.events 24 | assert tx.events["LearnerRegistered"]["courseId"] == 0 25 | 26 | 27 | def test_create_courses(contracts, steward): 28 | deschool, learning_curve = contracts 29 | for n in range(5): 30 | tx = deschool.createCourse( 31 | constants_unit.STAKE, 32 | constants_unit.DURATION, 33 | constants_unit.URL, 34 | constants_unit.CREATOR, 35 | {"from": steward} 36 | ) 37 | 38 | assert "CourseCreated" in tx.events 39 | assert tx.events["CourseCreated"]["courseId"] == n 40 | assert tx.events["CourseCreated"]["stake"] == constants_unit.STAKE 41 | assert tx.events["CourseCreated"]["duration"] == constants_unit.DURATION 42 | assert tx.events["CourseCreated"]["url"] == constants_unit.URL 43 | assert tx.events["CourseCreated"]["creator"] == constants_unit.CREATOR 44 | 45 | 46 | def test_create_malicious_courses(contracts, hackerman): 47 | deschool, learning_curve = contracts 48 | with brownie.reverts("createCourse: stake must be greater than 0"): 49 | deschool.createCourse( 50 | 0, 51 | constants_unit.DURATION, 52 | constants_unit.URL, 53 | constants_unit.CREATOR, 54 | {"from": hackerman} 55 | ) 56 | with brownie.reverts("createCourse: duration must be greater than 0"): 57 | deschool.createCourse( 58 | constants_unit.STAKE, 59 | 0, 60 | constants_unit.URL, 61 | constants_unit.CREATOR, 62 | {"from": hackerman} 63 | ) 64 | 65 | 66 | def test_register(contracts_with_courses, learners, token, deployer): 67 | deschool, learning_curve = contracts_with_courses 68 | 69 | for n, learner in enumerate(learners): 70 | token.transfer( 71 | learner, 72 | constants_unit.STAKE, 73 | {"from": deployer} 74 | ) 75 | token.approve(deschool, constants_unit.STAKE, {"from": learner}) 76 | before_bal = token.balanceOf(deschool) 77 | tx = deschool.register(0, {"from": learner}) 78 | 79 | assert "LearnerRegistered" in tx.events 80 | assert tx.events["LearnerRegistered"]["courseId"] == 0 81 | assert before_bal + constants_unit.STAKE == token.balanceOf(deschool) 82 | 83 | assert deschool.getCurrentBatchTotal() == constants_unit.STAKE * len(learners) 84 | assert token.balanceOf(deschool) == constants_unit.STAKE * len(learners) 85 | 86 | 87 | def test_register_malicious(contracts_with_courses, token, deployer, hackerman): 88 | deschool, learning_curve = contracts_with_courses 89 | with brownie.reverts(): 90 | deschool.register(0, {"from": hackerman}) 91 | 92 | token.transfer(hackerman, constants_unit.STAKE, {"from": deployer}) 93 | token.approve(deschool, constants_unit.STAKE, {"from": hackerman}) 94 | with brownie.reverts("register: courseId does not exist"): 95 | deschool.register(999, {"from": hackerman}) 96 | 97 | with brownie.reverts("register: courseId does not exist"): 98 | deschool.register(deschool.getNextCourseId(), {"from": hackerman}) 99 | 100 | deschool.register(0, {"from": hackerman}) 101 | token.transfer(hackerman, constants_unit.STAKE, {"from": deployer}) 102 | token.approve(deschool, constants_unit.STAKE, {"from": hackerman}) 103 | with brownie.reverts("register: already registered"): 104 | deschool.register(0, {"from": hackerman}) 105 | 106 | 107 | def test_mint(contracts_with_learners, learners, token, deployer): 108 | deschool, learning_curve = contracts_with_learners 109 | brownie.chain.mine(constants_unit.DURATION) 110 | for n, learner in enumerate(learners): 111 | assert deschool.verify(learner, 0) 112 | mintable_balance = learning_curve.getMintableForReserveAmount(constants_unit.STAKE) 113 | lc_dai_balance = token.balanceOf(learning_curve) 114 | ds_dai_balance = token.balanceOf(deschool) 115 | learner_lc_balance = learning_curve.balanceOf(learner) 116 | tx = deschool.mint(0, {"from": learner}) 117 | assert "LearnMintedFromCourse" in tx.events 118 | assert tx.events["LearnMintedFromCourse"]["learnMinted"] == mintable_balance 119 | assert tx.events["LearnMintedFromCourse"]["stableConverted"] == constants_unit.STAKE 120 | assert token.balanceOf(learning_curve) == lc_dai_balance + constants_unit.STAKE 121 | assert token.balanceOf(deschool) == ds_dai_balance - constants_unit.STAKE 122 | assert learning_curve.balanceOf(learner) == learner_lc_balance + mintable_balance 123 | 124 | 125 | def test_mint_malicious(contracts_with_learners, hackerman, learners): 126 | deschool, learning_curve = contracts_with_learners 127 | with brownie.reverts("mint: not a learner on this course"): 128 | deschool.mint(0, {"from": hackerman}) 129 | brownie.chain.mine(constants_unit.COURSE_RUNNING) 130 | with brownie.reverts("mint: not yet eligible - wait for the full course duration to pass"): 131 | deschool.mint(0, {"from": learners[0]}) 132 | 133 | 134 | def test_redeem(contracts_with_learners, learners, token, kernelTreasury): 135 | deschool, learning_curve = contracts_with_learners 136 | brownie.chain.mine(constants_unit.DURATION) 137 | for n, learner in enumerate(learners): 138 | kt_dai_balance = token.balanceOf(kernelTreasury) 139 | ds_dai_balance = token.balanceOf(deschool) 140 | tx = deschool.redeem(0, {"from": learner}) 141 | assert "StakeRedeemed" in tx.events 142 | assert tx.events["StakeRedeemed"]["amount"] == constants_unit.STAKE 143 | assert deschool.verify(learner, 0) 144 | assert kt_dai_balance == token.balanceOf(kernelTreasury) 145 | assert token.balanceOf(deschool) == ds_dai_balance - constants_unit.STAKE 146 | assert token.balanceOf(learner) == constants_unit.STAKE 147 | 148 | 149 | def test_redeem_malicious(hackerman, contracts_with_learners, learners): 150 | deschool, learning_curve = contracts_with_learners 151 | with brownie.reverts("redeem: not a learner on this course"): 152 | deschool.redeem(0, {"from": hackerman}) 153 | brownie.chain.mine(constants_unit.COURSE_RUNNING) 154 | with brownie.reverts("redeem: not yet eligible - wait for the full course duration to pass"): 155 | deschool.redeem(0, {"from": learners[0]}) 156 | 157 | 158 | def test_verify(contracts_with_learners, learners): 159 | deschool, learning_curve = contracts_with_learners 160 | learner = learners[0] 161 | assert not(deschool.verify(learner, 0, {"from": learner})) 162 | brownie.chain.mine(constants_unit.DURATION) 163 | assert deschool.verify(learner, 0, {"from": learner}) 164 | 165 | 166 | def test_verify_malicious(contracts_with_learners, hackerman, learners): 167 | deschool, learning_curve = contracts_with_learners 168 | learner = learners[0] 169 | with brownie.reverts("verify: courseId does not exist"): 170 | deschool.verify(learner, 999, {"from": hackerman}) 171 | with brownie.reverts("verify: courseId does not exist"): 172 | deschool.verify(learner, deschool.getNextCourseId(), {"from": hackerman}) 173 | with brownie.reverts("verify: not registered to this course"): 174 | deschool.verify(hackerman, 0, {"from": hackerman}) 175 | 176 | 177 | def test_mint_lc_not_initialised(token, deployer, steward, learners): 178 | learning_curve = LearningCurve.deploy(token.address, {"from": deployer}) 179 | deschool = DeSchool.deploy( 180 | token.address, 181 | learning_curve.address, 182 | constants_unit.VAULT, 183 | {"from": deployer} 184 | ) 185 | deschool.createCourse( 186 | constants_unit.STAKE, 187 | constants_unit.DURATION, 188 | constants_unit.URL, 189 | constants_unit.CREATOR, 190 | {"from": steward} 191 | ) 192 | for n, learner in enumerate(learners): 193 | token.transfer( 194 | learner, 195 | constants_unit.STAKE, 196 | {"from": deployer} 197 | ) 198 | token.approve(deschool, constants_unit.STAKE, {"from": learner}) 199 | deschool.register(0, {"from": learner}) 200 | brownie.chain.mine(constants_unit.DURATION) 201 | with brownie.reverts("!initialised"): 202 | deschool.mint(0, {"from": learner}) 203 | 204 | 205 | def build_permit(holder, spender, token, expiry): 206 | data = { 207 | "types": { 208 | "EIP712Domain": [ 209 | {"name": "name", "type": "string"}, 210 | {"name": "version", "type": "string"}, 211 | {"name": "chainId", "type": "uint256"}, 212 | {"name": "verifyingContract", "type": "address"}, 213 | ], 214 | "Permit": [ 215 | {"name": "holder", "type": "address"}, 216 | {"name": "spender", "type": "address"}, 217 | {"name": "nonce", "type": "uint256"}, 218 | {"name": "expiry", "type": "uint256"}, 219 | {"name": "allowed", "type": "bool"}, 220 | ], 221 | }, 222 | "domain": { 223 | "name": token.name(), 224 | "version": token.version(), 225 | "chainId": 1, 226 | "verifyingContract": str(token), 227 | }, 228 | "primaryType": "Permit", 229 | "message": { 230 | "holder": holder, 231 | "spender": spender, 232 | "nonce": token.nonces(holder), 233 | "expiry": 0, 234 | "allowed": True, 235 | }, 236 | } 237 | assert encode_hex(hash_domain(data)) == token.DOMAIN_SEPARATOR() 238 | return encode_structured_data(data) -------------------------------------------------------------------------------- /tests/test_unit_lc.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | from brownie import LearningCurve 3 | import constants_unit 4 | from math import log as ln 5 | 6 | from eth_account import Account 7 | from eth_account._utils.structured_data.hashing import hash_domain 8 | from eth_account.messages import encode_structured_data 9 | from eth_utils import encode_hex 10 | 11 | def test_flash_behaviour(token, deployer, hackerman, contracts, learners): 12 | _, learning_curve = contracts 13 | token.transfer( 14 | hackerman, 15 | constants_unit.MALICIOUS_AMOUNT, 16 | {"from": deployer} 17 | ) 18 | token.approve(learning_curve, constants_unit.MALICIOUS_AMOUNT, {"from": hackerman}) 19 | learning_curve.mint(constants_unit.MALICIOUS_AMOUNT, {"from": hackerman}) 20 | token_bal_before = token.balanceOf(hackerman) 21 | learn_bal_before = learning_curve.balanceOf(hackerman) 22 | print("Token Balance after mint: " + str(token_bal_before)) 23 | print("LEARN balance after mint: " + str(learn_bal_before)) 24 | 25 | for learner in learners: 26 | token.transfer( 27 | learner, 28 | constants_unit.MINT_AMOUNT, 29 | {"from": deployer} 30 | ) 31 | token.approve(learning_curve, constants_unit.MINT_AMOUNT, {"from": learner}) 32 | learning_curve.mint(constants_unit.MINT_AMOUNT, {"from": learner}) 33 | n = 0 34 | for learner in reversed(learners): 35 | lc_before_bal = token.balanceOf(learning_curve) 36 | learner_before_dai_bal = token.balanceOf(learner) 37 | learner_before_lc_bal = learning_curve.balanceOf(learner) 38 | print("Learning curve balance before: " + str(learning_curve.totalSupply())) 39 | print("Learning curve dai balance before: " + str(lc_before_bal)) 40 | print("Learner LEARN balance before: " + str(learner_before_lc_bal)) 41 | print("Learner Dai balance before: " + str(learner_before_dai_bal)) 42 | tx = learning_curve.burn(learning_curve.balanceOf(learner), {"from": learner}) 43 | print("Learning curve balance: " + str(learning_curve.totalSupply())) 44 | print("Learning curve dai balance: " + str(lc_before_bal)) 45 | print("Learner LEARN balance: " + str(learner_before_lc_bal)) 46 | print("Learner Dai balance: " + str(learner_before_dai_bal)) 47 | print("Learner Token Diff: " + str(tx.events["LearnBurned"]["daiReturned"] - constants_unit.MINT_AMOUNT)) 48 | print("Learner LEARN Diff: " + str(tx.events["LearnBurned"]["amountBurned"] - learner_before_lc_bal)) 49 | tx = learning_curve.burn(learning_curve.balanceOf(hackerman), {"from": hackerman}) 50 | print("Token Balance after burn: " + str(token.balanceOf(hackerman))) 51 | print("LEARN balance after burn: " + str(learning_curve.balanceOf(hackerman))) 52 | print("Hackerman Token Diff: " + str(tx.events["LearnBurned"]["daiReturned"] - constants_unit.MALICIOUS_AMOUNT)) 53 | print("Hackerman LEARN Diff: " + str(learn_bal_before - tx.events["LearnBurned"]["amountBurned"])) 54 | print("Final learning curve balance: " + str(learning_curve.totalSupply())) 55 | print("Final learning curve dai balance: " + str(token.balanceOf(learning_curve))) 56 | 57 | 58 | def test_init(token, deployer): 59 | learning_curve = LearningCurve.deploy(token.address, {"from": deployer}) 60 | token.approve(learning_curve, 1e18, {"from": deployer}) 61 | learning_curve.initialise({"from": deployer}) 62 | assert token.balanceOf(learning_curve) == 1e18 63 | assert learning_curve.totalSupply() == 10001e18 64 | assert learning_curve.reserveBalance() == 1e18 65 | 66 | 67 | def test_init_malicious(token, deployer, hackerman): 68 | learning_curve = LearningCurve.deploy(token.address, {"from": deployer}) 69 | with brownie.reverts(): 70 | learning_curve.initialise({"from": hackerman}) 71 | with brownie.reverts(): 72 | learning_curve.initialise({"from": deployer}) 73 | token.approve(learning_curve, 1e18, {"from": deployer}) 74 | learning_curve.initialise({"from": deployer}) 75 | with brownie.reverts("initialised"): 76 | learning_curve.initialise({"from": hackerman}) 77 | 78 | 79 | def test_mint(learners, token, deployer, contracts): 80 | _, learning_curve = contracts 81 | for n, learner in enumerate(learners): 82 | token.transfer( 83 | learner, 84 | constants_unit.MINT_AMOUNT, 85 | {"from": deployer} 86 | ) 87 | token.approve(learning_curve, constants_unit.MINT_AMOUNT, {"from": learner}) 88 | before_bal = token.balanceOf(learning_curve) 89 | learner_before_dai_bal = token.balanceOf(learner) 90 | learner_before_lc_bal = learning_curve.balanceOf(learner) 91 | numerator = float((learning_curve.reserveBalance() / 1e18 + constants_unit.MINT_AMOUNT / 1e18)) 92 | predicted_mint = float(constants_unit.K * ln(numerator / (learning_curve.reserveBalance() / 1e18))) * 1e18 93 | lc_supply_before = learning_curve.totalSupply() 94 | assert abs(predicted_mint - learning_curve.getMintableForReserveAmount(constants_unit.MINT_AMOUNT)) < \ 95 | constants_unit.ACCURACY 96 | learning_curve.mint(constants_unit.MINT_AMOUNT, {"from": learner}) 97 | assert abs(learner_before_lc_bal + predicted_mint - learning_curve.balanceOf(learner)) < constants_unit.ACCURACY 98 | assert learner_before_dai_bal - constants_unit.MINT_AMOUNT == token.balanceOf(learner) 99 | assert before_bal + constants_unit.MINT_AMOUNT == token.balanceOf( 100 | learning_curve) == learning_curve.reserveBalance() 101 | assert abs(learning_curve.totalSupply() - (predicted_mint + lc_supply_before)) < constants_unit.ACCURACY 102 | 103 | def test_mint_permit(token, deployer, contracts): 104 | _, learning_curve = contracts 105 | signer = Account.create() 106 | holder = signer.address 107 | token.transfer(holder, constants_unit.MINT_AMOUNT, {"from": deployer}) 108 | assert token.balanceOf(holder) == constants_unit.MINT_AMOUNT 109 | permit = build_permit(holder, str(learning_curve), token, 3600) 110 | signed = signer.sign_message(permit) 111 | print(token.balanceOf(learning_curve.address)) 112 | before_bal = token.balanceOf(learning_curve) 113 | learner_before_dai_bal = token.balanceOf(holder) 114 | learner_before_lc_bal = learning_curve.balanceOf(holder) 115 | numerator = float((learning_curve.reserveBalance() / 1e18 + constants_unit.MINT_AMOUNT / 1e18)) 116 | predicted_mint = float(constants_unit.K * ln(numerator / (learning_curve.reserveBalance() / 1e18))) * 1e18 117 | lc_supply_before = learning_curve.totalSupply() 118 | assert abs(predicted_mint - learning_curve.getMintableForReserveAmount(constants_unit.MINT_AMOUNT)) < \ 119 | constants_unit.ACCURACY 120 | tx = learning_curve.permitAndMint(constants_unit.MINT_AMOUNT, 0, 0, signed.v, signed.r, signed.s, {"from": holder}) 121 | print(token.balanceOf(learning_curve.address)) 122 | 123 | assert abs(learner_before_lc_bal + predicted_mint - learning_curve.balanceOf(holder)) < constants_unit.ACCURACY 124 | assert learner_before_dai_bal - constants_unit.MINT_AMOUNT == token.balanceOf(holder) 125 | assert before_bal + constants_unit.MINT_AMOUNT == token.balanceOf( 126 | learning_curve) == learning_curve.reserveBalance() 127 | assert abs(learning_curve.totalSupply() - (predicted_mint + lc_supply_before)) < constants_unit.ACCURACY 128 | 129 | 130 | def test_burn(learners, token, deployer, contracts): 131 | _, learning_curve = contracts 132 | for learner in learners: 133 | token.transfer( 134 | learner, 135 | constants_unit.MINT_AMOUNT, 136 | {"from": deployer} 137 | ) 138 | token.approve(learning_curve, constants_unit.MINT_AMOUNT, {"from": learner}) 139 | learning_curve.mint(constants_unit.MINT_AMOUNT, {"from": learner}) 140 | 141 | n = 0 142 | for learner in reversed(learners): 143 | before_bal = token.balanceOf(learning_curve) 144 | learner_before_dai_bal = token.balanceOf(learner) 145 | learner_before_lc_bal = learning_curve.balanceOf(learner) 146 | numerator = float((learning_curve.reserveBalance() / 1e18)) 147 | predicted_burn = constants_unit.MINT_AMOUNT 148 | lc_supply_before = learning_curve.totalSupply() 149 | tx = learning_curve.burn(learning_curve.balanceOf(learner), {"from": learner}) 150 | assert abs(learner_before_lc_bal - tx.events["LearnBurned"]["amountBurned"] + learning_curve.balanceOf(learner)) < constants_unit.ACCURACY 151 | assert abs( 152 | learner_before_dai_bal + constants_unit.MINT_AMOUNT - token.balanceOf(learner)) <= constants_unit.ACCURACY 153 | assert before_bal - constants_unit.MINT_AMOUNT - token.balanceOf(learning_curve) <= constants_unit.ACCURACY 154 | assert before_bal - constants_unit.MINT_AMOUNT - learning_curve.reserveBalance() <= constants_unit.ACCURACY 155 | assert abs(learning_curve.totalSupply() + (learner_before_lc_bal - lc_supply_before)) < constants_unit.ACCURACY 156 | 157 | def build_permit(holder, spender, token, expiry): 158 | data = { 159 | "types": { 160 | "EIP712Domain": [ 161 | {"name": "name", "type": "string"}, 162 | {"name": "version", "type": "string"}, 163 | {"name": "chainId", "type": "uint256"}, 164 | {"name": "verifyingContract", "type": "address"}, 165 | ], 166 | "Permit": [ 167 | {"name": "holder", "type": "address"}, 168 | {"name": "spender", "type": "address"}, 169 | {"name": "nonce", "type": "uint256"}, 170 | {"name": "expiry", "type": "uint256"}, 171 | {"name": "allowed", "type": "bool"}, 172 | ], 173 | }, 174 | "domain": { 175 | "name": token.name(), 176 | "version": token.version(), 177 | "chainId": 1, 178 | "verifyingContract": str(token), 179 | }, 180 | "primaryType": "Permit", 181 | "message": { 182 | "holder": holder, 183 | "spender": spender, 184 | "nonce": token.nonces(holder), 185 | "expiry": 0, 186 | "allowed": True, 187 | }, 188 | } 189 | assert encode_hex(hash_domain(data)) == token.DOMAIN_SEPARATOR() 190 | return encode_structured_data(data) --------------------------------------------------------------------------------