├── .gitignore ├── contracts ├── Migrations.sol ├── interface │ ├── FPrice.sol │ └── SFC.sol └── liquiditypool │ └── LiquidityPool.sol ├── package-lock.json ├── package.json ├── test └── LiquidityPool.js ├── truffle-config.js └── utils ├── EVMThrow.js ├── asserts.js ├── ether.js ├── index.js └── timeController.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 63 | 64 | # dependencies 65 | /node_modules 66 | /.pnp 67 | .pnp.js 68 | 69 | # testing 70 | /coverage 71 | 72 | # production 73 | /build 74 | 75 | # misc 76 | .DS_Store 77 | .env.local 78 | .env.development.local 79 | .env.test.local 80 | .env.production.local 81 | 82 | npm-debug.log* 83 | yarn-debug.log* 84 | yarn-error.log* 85 | -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | 4 | contract Migrations { 5 | address public owner; 6 | uint public lastCompletedMigration; 7 | 8 | modifier restricted() { 9 | if (msg.sender == owner) { 10 | _; 11 | } 12 | } 13 | 14 | constructor() public { 15 | owner = msg.sender; 16 | } 17 | 18 | function setCompleted(uint completed) public restricted { 19 | lastCompletedMigration = completed; 20 | } 21 | 22 | function upgrade(address newAddress) public restricted { 23 | Migrations upgraded = Migrations(newAddress); 24 | upgraded.setCompleted(lastCompletedMigration); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /contracts/interface/FPrice.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | interface IFPrice { 4 | 5 | function getPrice(address _token) external view returns (uint256); 6 | function getLiquidity(address _token) external view returns (uint256); 7 | 8 | } 9 | -------------------------------------------------------------------------------- /contracts/interface/SFC.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | interface SFC { 4 | function calcDelegationRewards(address delegator, uint256 _fromEpoch, uint256 maxEpochs) external view returns (uint256, uint256, uint256); 5 | function calcValidatorRewards(uint256 stakerID, uint256 _fromEpoch, uint256 maxEpochs) external view returns (uint256, uint256, uint256); 6 | function getStakerID(address addr) external view returns (uint256); 7 | } 8 | -------------------------------------------------------------------------------- /contracts/liquiditypool/LiquidityPool.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | import "@openzeppelin/contracts/math/SafeMath.sol"; 4 | import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; 5 | import "@openzeppelin/contracts/utils/Address.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 7 | import "@openzeppelin/contracts/token/ERC20/ERC20Mintable.sol"; 8 | import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; 9 | import "../interface/FPrice.sol"; 10 | import "../interface/SFC.sol"; 11 | 12 | contract LiquidityPool is ReentrancyGuard { 13 | using SafeMath for uint256; 14 | using Address for address; 15 | using SafeERC20 for ERC20; 16 | 17 | struct EpochSnapshot { 18 | uint256 endTime; 19 | uint256 duration; 20 | uint256 feePool; 21 | } 22 | 23 | uint256 currentEpoch; 24 | mapping(uint256 => EpochSnapshot) public epochSnapshots; // written by consensus outside 25 | 26 | function _makeEpochSnapshot() external { 27 | currentEpoch++; 28 | EpochSnapshot storage newSnapshot = epochSnapshots[currentEpoch]; 29 | newSnapshot.endTime = block.timestamp; 30 | if (currentEpoch == 0) { 31 | newSnapshot.duration = 0; 32 | } else { 33 | newSnapshot.duration = block.timestamp - epochSnapshots[currentEpoch - 1].endTime; 34 | } 35 | newSnapshot.feePool = feePool; 36 | feePool = 0; 37 | } 38 | 39 | 40 | uint256 public feePool; 41 | 42 | // Collateral data 43 | mapping(address => mapping(address => uint256)) public _collateral; 44 | mapping(address => mapping(address => uint256)) public _collateralTokens; 45 | mapping(address => address[]) public _collateralList; 46 | //Collateral measured in fUSD 47 | mapping(address => uint256) public _collateralValue; 48 | 49 | // Debt data 50 | mapping(address => mapping(address => uint256)) public _debt; 51 | mapping(address => mapping(address => uint256)) public _debtTokens; 52 | mapping(address => address[]) public _debtList; 53 | // Debt measured in fUSD 54 | mapping(address => uint256) public _debtValue; 55 | 56 | // Claimed balances 57 | mapping (address => uint256) public _claimedEpoch; 58 | mapping (address => uint256) public _claimed; 59 | 60 | //native denom address 61 | function fAddress() internal pure returns(address) { 62 | return 0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF; 63 | } 64 | 65 | //ftmAddress 66 | function SFCAddress() internal pure returns(address) { 67 | return 0xFC00FACE00000000000000000000000000000000; 68 | } 69 | 70 | //oracleAddress 71 | function oAddress() internal pure returns(address) { 72 | return 0xC17518AE5dAD82B8fc8b56Fe0295881c30848829; 73 | } 74 | 75 | //fUSD 76 | function fUSD() internal pure returns(address) { 77 | return 0xC17518AE5dAD82B8fc8b56Fe0295881c30848829; 78 | } 79 | 80 | // Tracks list of user collateral 81 | function addCollateralToList(address _token, address _owner) internal { 82 | bool tokenAlreadyAdded = false; 83 | address[] memory tokenList = _collateralList[_owner]; 84 | for (uint256 i = 0; i < tokenList.length; i++) 85 | if (tokenList[i] == _token) { 86 | tokenAlreadyAdded = true; 87 | } 88 | if (!tokenAlreadyAdded) _collateralList[_owner].push(_token); 89 | } 90 | 91 | // Tracks list of user debt 92 | function addDebtToList(address _token, address _owner) internal { 93 | bool tokenAlreadyAdded = false; 94 | address[] memory tokenList = _debtList[_owner]; 95 | for (uint256 i = 0; i < tokenList.length; i++) 96 | if (tokenList[i] == _token) { 97 | tokenAlreadyAdded = true; 98 | } 99 | if (!tokenAlreadyAdded) _debtList[_owner].push(_token); 100 | } 101 | 102 | event Claim( 103 | address indexed _token, 104 | address indexed _user, 105 | uint256 _amount, 106 | uint256 _timestamp 107 | ); 108 | 109 | event Deposit( 110 | address indexed _token, 111 | address indexed _user, 112 | uint256 _amount, 113 | uint256 _timestamp 114 | ); 115 | 116 | event Withdraw( 117 | address indexed _token, 118 | address indexed _user, 119 | uint256 _amount, 120 | uint256 _timestamp 121 | ); 122 | 123 | event Borrow( 124 | address indexed _token, 125 | address indexed _user, 126 | uint256 _amount, 127 | uint256 _timestamp 128 | ); 129 | 130 | event Repay( 131 | address indexed _token, 132 | address indexed _user, 133 | uint256 _amount, 134 | uint256 _timestamp 135 | ); 136 | 137 | event Mint( 138 | address indexed _token, 139 | address indexed _user, 140 | uint256 _amount, 141 | uint256 _timestamp 142 | ); 143 | 144 | event Burn( 145 | address indexed _token, 146 | address indexed _user, 147 | uint256 _amount, 148 | uint256 _timestamp 149 | ); 150 | 151 | event Buy( 152 | address indexed _token, 153 | address indexed _user, 154 | uint256 _amount, 155 | uint256 _price, 156 | uint256 _timestamp 157 | ); 158 | 159 | event Sell( 160 | address indexed _token, 161 | address indexed _user, 162 | uint256 _amount, 163 | uint256 _price, 164 | uint256 _timestamp 165 | ); 166 | 167 | // Calculate the current collateral value of all assets for user 168 | function calcCollateralValue(address addr) public view returns(uint256 collateralValue) 169 | { 170 | for (uint i = 0; i < _collateralList[addr].length; i++) { 171 | uint256 results = IFPrice(oAddress()).getPrice(_collateralList[addr][i]); 172 | collateralValue = collateralValue.add(results); 173 | } 174 | collateralValue = collateralValue.div(2); 175 | } 176 | 177 | // Calculate the current debt value of all assets for user 178 | function calcDebtValue(address addr) public view returns(uint256 debtValue) 179 | { 180 | for (uint i = 0; i < _debtList[addr].length; i++) { 181 | uint256 results = IFPrice(oAddress()).getPrice(_debtList[addr][i]); 182 | debtValue = debtValue.add(results); 183 | } 184 | } 185 | 186 | // Claim rewards in fUSD off of locked native denom 187 | // Fee 0.25% 188 | function claimDelegationRewards(uint256 maxEpochs) external nonReentrant { 189 | require(maxEpochs > 0, "only positive epochs"); 190 | // Get current fUSD value of native denom 191 | uint256 tokenValue = IFPrice(oAddress()).getPrice(fAddress()); 192 | require(tokenValue > 0, "native denom has no value"); 193 | 194 | uint256 fromEpoch = _claimedEpoch[msg.sender]; 195 | (uint256 pendingRewards, , uint256 untilEpoch) = SFC(SFCAddress()).calcDelegationRewards(msg.sender, fromEpoch, maxEpochs); 196 | require(pendingRewards > 0, "no pending rewards"); 197 | 198 | _claimed[msg.sender] = pendingRewards.add(_claimed[msg.sender]); 199 | _claimedEpoch[msg.sender] = untilEpoch; 200 | 201 | // 300% collateral value 202 | uint256 _amount = pendingRewards.mul(tokenValue).div(3); 203 | uint256 fee = _amount.mul(25).div(10000); 204 | feePool = feePool.add(fee); 205 | 206 | // Mint 50% worth of fUSD and transfer 207 | ERC20Mintable(fUSD()).mint(address(this), _amount); 208 | ERC20(fUSD()).safeTransfer(msg.sender, _amount.sub(fee)); 209 | 210 | emit Claim(fUSD(), msg.sender, _amount, block.timestamp); 211 | } 212 | 213 | // Claim validator rewards in fUSD off of locked native denom 214 | // Fee 0.25% 215 | function claimValidatorRewards(uint256 maxEpochs) external nonReentrant { 216 | require(maxEpochs > 0, "only positive epochs"); 217 | 218 | uint256 tokenValue = IFPrice(oAddress()).getPrice(fAddress()); 219 | require(tokenValue > 0, "native denom has no value"); 220 | uint256 fromEpoch = _claimedEpoch[msg.sender]; 221 | uint256 stakerID = SFC(SFCAddress()).getStakerID(msg.sender); 222 | (uint256 pendingRewards, , uint256 untilEpoch) = SFC(SFCAddress()).calcValidatorRewards(stakerID, fromEpoch, maxEpochs); 223 | require(pendingRewards > 0, "no pending rewards"); 224 | 225 | _claimed[msg.sender] = pendingRewards.add(_claimed[msg.sender]); 226 | _claimedEpoch[msg.sender] = untilEpoch; 227 | 228 | uint256 _amount = pendingRewards.mul(tokenValue).div(3); 229 | uint256 fee = _amount.mul(25).div(10000); 230 | feePool = feePool.add(fee); 231 | 232 | ERC20Mintable(fUSD()).mint(address(this), _amount); 233 | ERC20(fUSD()).safeTransfer(msg.sender, _amount.sub(fee)); 234 | 235 | emit Claim(fUSD(), msg.sender, _amount, block.timestamp); 236 | } 237 | 238 | // Deposit assets as collateral 239 | // No fee, gain interest on deposited value 240 | function deposit(address _token, uint256 _amount) 241 | external 242 | payable 243 | nonReentrant 244 | { 245 | require(_amount > 0, "amount must be greater than 0"); 246 | 247 | // Mapping of token => users 248 | // Mapping of users => tokens 249 | 250 | _collateral[_token][msg.sender] = _collateral[_token][msg.sender].add(_amount); 251 | _collateralTokens[msg.sender][_token] = _collateralTokens[msg.sender][_token].add(_amount); 252 | addCollateralToList(_token, msg.sender); 253 | 254 | _collateralValue[msg.sender] = calcCollateralValue(msg.sender); 255 | 256 | // Non native denom 257 | if (_token != fAddress()) { 258 | require(msg.value == 0, "user is sending native denom along with the token transfer."); 259 | ERC20(_token).safeTransferFrom(msg.sender, address(this), _amount); 260 | } else { 261 | require(msg.value >= _amount, "the amount and the value sent to deposit do not match"); 262 | if (msg.value > _amount) { 263 | uint256 excessAmount = msg.value.sub(_amount); 264 | (bool result, ) = msg.sender.call.value(excessAmount).gas(50000)(""); 265 | require(result, "transfer of ETH failed"); 266 | } 267 | } 268 | emit Deposit(_token, msg.sender, _amount, block.timestamp); 269 | } 270 | 271 | // Withdraw any deposited collateral that has a value from the contract 272 | // No fee 273 | function withdraw(address _token, uint256 _amount) 274 | external 275 | nonReentrant 276 | { 277 | require(_amount > 0, "amount must be greater than 0"); 278 | 279 | _collateral[_token][msg.sender] = _collateral[_token][msg.sender].sub(_amount, "withdraw amount exceeds balance"); 280 | _collateralTokens[msg.sender][_token] = _collateralTokens[msg.sender][_token].sub(_amount, "withdraw amount exceeds balance"); 281 | 282 | uint256 collateralValue = calcCollateralValue(msg.sender); 283 | uint256 debtValue = calcDebtValue(msg.sender); 284 | 285 | require(collateralValue > debtValue, "withdraw would liquidate holdings"); 286 | 287 | _collateralValue[msg.sender] = collateralValue; 288 | _debtValue[msg.sender] = debtValue; 289 | 290 | if (_token != fAddress()) { 291 | uint256 balance = ERC20(_token).balanceOf(address(this)); 292 | if (balance < _amount) { 293 | ERC20Mintable(_token).mint(address(this), _amount.sub(balance)); 294 | } 295 | ERC20(_token).safeTransfer(msg.sender, _amount); 296 | } else { 297 | (bool result, ) = msg.sender.call.value(_amount).gas(50000)(""); 298 | require(result, "transfer of ETH failed"); 299 | } 300 | emit Withdraw(_token, msg.sender, _amount, block.timestamp); 301 | } 302 | 303 | // Fee 0.25% 304 | // Can't buy or sell native denom or fUSD 305 | function buy(address _token, uint256 _amount) 306 | external 307 | nonReentrant 308 | { 309 | require(_amount > 0, "amount must be greater than 0"); 310 | require(_token != fAddress(), "native denom"); 311 | require(_token != fUSD(), "fusd denom"); 312 | 313 | uint256 tokenValue = IFPrice(oAddress()).getPrice(_token); 314 | require(tokenValue > 0, "token has no value"); 315 | 316 | uint256 buyValue = _amount.mul(tokenValue); 317 | uint256 fee = buyValue.mul(25).div(10000); 318 | uint256 buyValueIncFee = buyValue.add(fee); 319 | uint256 balance = ERC20(fUSD()).balanceOf(msg.sender); 320 | require(balance >= buyValueIncFee, "insufficient funds"); 321 | 322 | // Claim fUSD 323 | ERC20(fUSD()).safeTransferFrom(msg.sender, address(this), buyValueIncFee); 324 | 325 | // Mint and transfer token 326 | ERC20Mintable(_token).mint(address(this), _amount); 327 | ERC20(_token).safeTransfer(msg.sender, _amount); 328 | 329 | feePool = feePool.add(fee); 330 | 331 | emit Buy(_token, msg.sender, _amount, tokenValue, block.timestamp); 332 | } 333 | 334 | // Sell an owned asset for fusd 335 | // Fee 0.25% 336 | // Can't buy or sell native denom or fUSD 337 | function sell(address _token, uint256 _amount) 338 | external 339 | nonReentrant 340 | { 341 | require(_amount > 0, "amount must be greater than 0"); 342 | require(_token != fAddress(), "native denom"); 343 | require(_token != fUSD(), "fusd denom"); 344 | 345 | uint256 balance = ERC20(_token).balanceOf(msg.sender); 346 | require(balance >= _amount, "insufficient funds"); 347 | 348 | uint256 tokenValue = IFPrice(oAddress()).getPrice(_token); 349 | require(tokenValue > 0, "token has no value"); 350 | 351 | uint256 sellValue = _amount.mul(tokenValue); 352 | 353 | // Claim token 354 | ERC20(_token).safeTransferFrom(msg.sender, address(this), _amount); 355 | 356 | // Mint and transfer fUSD 357 | balance = ERC20(_token).balanceOf(address(this)); 358 | if (balance < sellValue) { 359 | // Mint fUSD and increase global debt 360 | // This is the value of fUSD to native denom ratio 361 | // totalSupply(fUSD) / 2 = claimable value of native denom 362 | ERC20Mintable(fUSD()).mint(address(this), sellValue.sub(balance)); 363 | } 364 | uint256 fee = sellValue.mul(25).div(10000); 365 | feePool = feePool.add(fee); 366 | ERC20(fUSD()).safeTransfer(msg.sender, sellValue.sub(fee)); 367 | 368 | emit Sell(_token, msg.sender, _amount, tokenValue, block.timestamp); 369 | } 370 | 371 | // Fee 4% per annum 372 | // Initiation fee 0.25% of total value 373 | function borrow(address _token, uint256 _amount) 374 | external 375 | nonReentrant 376 | { 377 | require(_amount > 0, "amount must be greater than 0"); 378 | require(_token != fAddress(), "native denom not borrowable"); 379 | require(_collateralValue[msg.sender] > 0, "collateral must be greater than 0"); 380 | 381 | uint256 tokenValue = IFPrice(oAddress()).getPrice(_token); 382 | require(tokenValue > 0, "debt token has no value"); 383 | 384 | // Calculate 0.25% initiation fee 385 | uint256 fee = _amount.mul(tokenValue).mul(25).div(10000); 386 | feePool = feePool.add(fee); 387 | _debt[fUSD()][msg.sender] = _debt[fUSD()][msg.sender].add(fee); 388 | _debtTokens[msg.sender][fUSD()] = _debtTokens[msg.sender][fUSD()].add(fee); 389 | addDebtToList(fUSD(), msg.sender); 390 | 391 | _debt[_token][msg.sender] = _debt[_token][msg.sender].add(_amount); 392 | _debtTokens[msg.sender][_token] = _debtTokens[msg.sender][_token].add(_amount); 393 | addDebtToList(_token, msg.sender); 394 | 395 | uint256 collateralValue = calcCollateralValue(msg.sender); 396 | uint256 debtValue = calcDebtValue(msg.sender); 397 | 398 | require(collateralValue > debtValue, "insufficient collateral"); 399 | 400 | _collateralValue[msg.sender] = collateralValue; 401 | _debtValue[msg.sender] = debtValue; 402 | 403 | uint256 balance = ERC20(_token).balanceOf(address(this)); 404 | if (balance < _amount) { 405 | ERC20Mintable(_token).mint(address(this), _amount.sub(balance)); 406 | } 407 | ERC20(_token).safeTransfer(msg.sender, _amount); 408 | 409 | emit Borrow(_token, msg.sender, _amount, block.timestamp); 410 | } 411 | 412 | // Fee 6% per annum 413 | // Initiation fee 0.25% of total value 414 | /*function mint(address _token, uint256 _amount) 415 | external 416 | nonReentrant 417 | { 418 | require(_amount > 0, "amount must be greater than 0"); 419 | require(_collateralValue[msg.sender] > 0, "collateral must be greater than 0"); 420 | require(_token != fAddress(), "native denom"); 421 | require(_token != fUSD(), "fusd denom"); 422 | 423 | uint256 tokenValue = IFPrice(oAddress()).getPrice(_token); 424 | require(tokenValue > 0, "token has no value"); 425 | 426 | // Calculate 0.25% initiation fee 427 | uint256 fee = _amount.mul(tokenValue).mul(25).div(10000); 428 | feePool = feePool.add(fee); 429 | _debt[fUSD()][msg.sender] = _debt[fUSD()][msg.sender].add(fee); 430 | _debtTokens[msg.sender][fUSD()] = _debtTokens[msg.sender][fUSD()].add(fee); 431 | addDebtToList(fUSD(), msg.sender); 432 | 433 | // Accure debt into debt values 434 | _debt[_token][msg.sender] = _debt[_token][msg.sender].add(_amount); 435 | _debtTokens[msg.sender][_token] = _debtTokens[msg.sender][_token].add(_amount); 436 | addDebtToList(_token, msg.sender); 437 | 438 | uint256 collateralValue = calcCollateralValue(msg.sender); 439 | uint256 debtValue = calcDebtValue(msg.sender); 440 | 441 | require(collateralValue > debtValue, "insufficient collateral"); 442 | 443 | _collateralValue[msg.sender] = collateralValue; 444 | _debtValue[msg.sender] = debtValue; 445 | 446 | ERC20Mintable(_token).mint(address(this), _amount); 447 | ERC20(_token).safeTransfer(msg.sender, _amount); 448 | 449 | emit Mint(_token, msg.sender, _amount, block.timestamp); 450 | }*/ 451 | 452 | // No fee 453 | /*function burn(address _token, uint256 _amount) 454 | external 455 | payable 456 | nonReentrant 457 | { 458 | require(_amount > 0, "amount must be greater than 0"); 459 | require(_token != fAddress(), "native denom"); 460 | 461 | _debt[_token][msg.sender] = _debt[_token][msg.sender].sub(_amount, "insufficient debt outstanding"); 462 | _debtTokens[msg.sender][_token] = _debtTokens[msg.sender][_token].sub(_amount, "insufficient debt outstanding"); 463 | 464 | _collateralValue[msg.sender] = calcCollateralValue(msg.sender); 465 | _debtValue[msg.sender] = calcDebtValue(msg.sender); 466 | 467 | ERC20(_token).safeTransferFrom(msg.sender, address(this), _amount); 468 | 469 | emit Burn(_token, msg.sender, _amount, block.timestamp); 470 | }*/ 471 | 472 | // No fee 473 | function repay(address _token, uint256 _amount) 474 | external 475 | payable 476 | nonReentrant 477 | { 478 | require(_amount > 0, "amount must be greater than 0"); 479 | require(_token != fAddress(), "native denom not borrowable"); 480 | 481 | 482 | _debt[_token][msg.sender] = _debt[_token][msg.sender].sub(_amount, "insufficient debt outstanding"); 483 | _debtTokens[msg.sender][_token] = _debtTokens[msg.sender][_token].sub(_amount, "insufficient debt outstanding"); 484 | 485 | _collateralValue[msg.sender] = calcCollateralValue(msg.sender); 486 | _debtValue[msg.sender] = calcDebtValue(msg.sender); 487 | 488 | require(msg.value == 0, "user is sending ETH along with the ERC20 transfer."); 489 | ERC20(_token).safeTransferFrom(msg.sender, address(this), _amount); 490 | 491 | emit Repay(_token, msg.sender, _amount, block.timestamp); 492 | } 493 | 494 | function liquidate(address _owner) 495 | external 496 | nonReentrant 497 | { 498 | _collateralValue[_owner] = calcCollateralValue(_owner); 499 | _debtValue[_owner] = calcDebtValue(_owner); 500 | 501 | require(_collateralValue[_owner] < _debtValue[_owner], "insufficient debt to liquidate"); 502 | 503 | for (uint i = 0; i < _collateralList[_owner].length; i++) { 504 | _collateral[_collateralList[_owner][i]][_owner] = _collateral[_collateralList[_owner][i]][_owner].sub(_collateralTokens[_owner][_collateralList[_owner][i]], "liquidation exceeds balance"); 505 | _collateralTokens[_owner][_collateralList[_owner][i]] = _collateralTokens[_owner][_collateralList[_owner][i]].sub(_collateralTokens[_owner][_collateralList[_owner][i]], "liquidation exceeds balance"); 506 | } 507 | 508 | for (uint i = 0; i < _debtList[_owner].length; i++) { 509 | _debt[_collateralList[_owner][i]][_owner] = _debt[_collateralList[_owner][i]][_owner].sub(_debt[_owner][_collateralList[_owner][i]], "liquidation exceeds balance"); 510 | _debtTokens[_owner][_collateralList[_owner][i]] = _debtTokens[_owner][_collateralList[_owner][i]].sub(_debtTokens[_owner][_collateralList[_owner][i]], "liquidation exceeds balance"); 511 | } 512 | 513 | _collateralValue[_owner] = calcCollateralValue(_owner); 514 | _debtValue[_owner] = calcDebtValue(_owner); 515 | } 516 | 517 | function liquidateToken(address _owner, address _token) 518 | external 519 | nonReentrant 520 | { 521 | _collateralValue[_owner] = calcCollateralValue(_owner); 522 | _debtValue[_owner] = calcDebtValue(_owner); 523 | 524 | require(_collateralValue[_owner] < _debtValue[_owner], "insufficient debt to liquidate"); 525 | 526 | _collateral[_token][_owner] = _collateral[_token][_owner].sub(_collateralTokens[_owner][_token], "liquidation exceeds balance"); 527 | _collateralTokens[_owner][_token] = _collateralTokens[_owner][_token].sub(_collateralTokens[_owner][_token], "liquidation exceeds balance"); 528 | 529 | _debt[_token][_owner] = _debt[_token][_owner].sub(_debt[_owner][_token], "liquidation exceeds balance"); 530 | _debtTokens[_owner][_token] = _debtTokens[_owner][_token].sub(_debtTokens[_owner][_token], "liquidation exceeds balance"); 531 | 532 | _collateralValue[_owner] = calcCollateralValue(_owner); 533 | _debtValue[_owner] = calcDebtValue(_owner); 534 | } 535 | } 536 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xar-dlp", 3 | "version": "1.0.0", 4 | "description": "fantom decentralized lending platform", 5 | "scripts": { 6 | "lint": "npm run eslint && npm run solium", 7 | "eslint": "eslint test/ migrations/", 8 | "solium": "solium --dir contracts", 9 | "build": "truffle build", 10 | "test": "truffle test", 11 | "truffle": "truffle", 12 | "flatten": "rm -rf contracts/Flattened.sol && truffle-flattener contracts/Staker.sol >> contracts/Flattened.sol" 13 | }, 14 | "dependencies": { 15 | "@openzeppelin/contracts": "^2.3.0" 16 | }, 17 | "devDependencies": { 18 | "babel-polyfill": "^6.26.0", 19 | "babel-preset-env": "^1.7.0", 20 | "babel-register": "^6.26.0", 21 | "bignumber.js": "^9.0.0", 22 | "chai": "^4.2.0", 23 | "chai-as-promised": "^7.1.1", 24 | "chai-bignumber": "^3.0.0", 25 | "eslint": "^6.2.2", 26 | "eslint-config-airbnb": "^18.0.1", 27 | "eslint-config-airbnb-base": "^14.0.0", 28 | "eslint-plugin-import": "^2.18.2", 29 | "eslint-plugin-jsx-a11y": "^6.2.3", 30 | "eslint-plugin-react": "^7.14.3", 31 | "eslint-plugin-react-hooks": "^1.7.0", 32 | "ethereumjs-testrpc-sc": "^6.5.1-sc.0", 33 | "karma": "^4.3.0", 34 | "lodash": "^4.17.15", 35 | "openzeppelin-test-helpers": "^0.4.3", 36 | "solidity-coverage": "^0.6.4", 37 | "truffle-flattener": "^1.4.0", 38 | "web3-utils": "^1.2.1" 39 | }, 40 | "repository": { 41 | "type": "git", 42 | "url": "" 43 | }, 44 | "author": "@andrecronje", 45 | "license": "MIT" 46 | } 47 | -------------------------------------------------------------------------------- /test/LiquidityPool.js: -------------------------------------------------------------------------------- 1 | const { 2 | BN, 3 | ether, 4 | expectRevert, 5 | time, 6 | balance, 7 | } = require('openzeppelin-test-helpers'); 8 | const { expect } = require('chai'); 9 | 10 | const LiquidityPool = artifacts.require('LiquidityPool'); 11 | 12 | contract('liquidity pool test', async () => { 13 | it('checking pool parameters', async () => { 14 | this.liquiditypool = await LiquidityPool.new(); 15 | expect(await this.liquiditypool.calcCollateralValue.call('0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF')).to.be.bignumber.equal(ether('0')); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /truffle-config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this file to configure your truffle project. It's seeded with some 3 | * common settings for different networks and features like migrations, 4 | * compilation and testing. Uncomment the ones you need or modify 5 | * them to suit your project as necessary. 6 | * 7 | * More information about configuration can be found at: 8 | * 9 | * truffleframework.com/docs/advanced/configuration 10 | * 11 | * To deploy via Infura you'll need a wallet provider (like truffle-hdwallet-provider) 12 | * to sign your transactions before they're sent to a remote public node. Infura accounts 13 | * are available for free at: infura.io/register. 14 | * 15 | * You'll also need a mnemonic - the twelve word phrase the wallet uses to generate 16 | * public/private key pairs. If you're publishing your code to GitHub make sure you load this 17 | * phrase from a file you've .gitignored so it doesn't accidentally become public. 18 | * 19 | */ 20 | 21 | // const HDWalletProvider = require('truffle-hdwallet-provider'); 22 | // const infuraKey = "fj4jll3k....."; 23 | // 24 | // const fs = require('fs'); 25 | // const mnemonic = fs.readFileSync(".secret").toString().trim(); 26 | 27 | module.exports = { 28 | /** 29 | * Networks define how you connect to your ethereum client and let you set the 30 | * defaults web3 uses to send transactions. If you don't specify one truffle 31 | * will spin up a development blockchain for you on port 9545 when you 32 | * run `develop` or `test`. You can ask a truffle command to use a specific 33 | * network from the command line, e.g 34 | * 35 | * $ truffle test --network 36 | */ 37 | 38 | networks: { 39 | // Useful for testing. The `development` name is special - truffle uses it by default 40 | // if it's defined here and no other network is specified at the command line. 41 | // You should run a client (like ganache-cli, geth or parity) in a separate terminal 42 | // tab if you use this network and you must also set the `host`, `port` and `network_id` 43 | // options below to some value. 44 | // 45 | // development: { 46 | // host: "127.0.0.1", // Localhost (default: none) 47 | // port: 8545, // Standard Ethereum port (default: none) 48 | // network_id: "*", // Any network (default: none) 49 | // }, 50 | 51 | // Another network with more advanced options... 52 | // advanced: { 53 | // port: 8777, // Custom port 54 | // network_id: 1342, // Custom network 55 | // gas: 8500000, // Gas sent with each transaction (default: ~6700000) 56 | // gasPrice: 20000000000, // 20 gwei (in wei) (default: 100 gwei) 57 | // from:
, // Account to send txs from (default: accounts[0]) 58 | // websockets: true // Enable EventEmitter interface for web3 (default: false) 59 | // }, 60 | 61 | // Useful for deploying to a public network. 62 | // NB: It's important to wrap the provider as a function. 63 | // ropsten: { 64 | // provider: () => new HDWalletProvider(mnemonic, `https://ropsten.infura.io/v3/YOUR-PROJECT-ID`), 65 | // network_id: 3, // Ropsten's id 66 | // gas: 5500000, // Ropsten has a lower block limit than mainnet 67 | // confirmations: 2, // # of confs to wait between deployments. (default: 0) 68 | // timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50) 69 | // skipDryRun: true // Skip dry run before migrations? (default: false for public nets ) 70 | // }, 71 | 72 | // Useful for private networks 73 | // private: { 74 | // provider: () => new HDWalletProvider(mnemonic, `https://network.io`), 75 | // network_id: 2111, // This network is yours, in the cloud. 76 | // production: true // Treats this network as if it was a public net. (default: false) 77 | // } 78 | }, 79 | 80 | // Set default mocha options here, use special reporters etc. 81 | mocha: { 82 | // timeout: 100000 83 | }, 84 | 85 | // Configure your compilers 86 | compilers: { 87 | solc: { 88 | // version: "0.5.1", // Fetch exact version from solc-bin (default: truffle's version) 89 | // docker: true, // Use "0.5.1" you've installed locally with docker (default: false) 90 | // settings: { // See the solidity docs for advice about optimization and evmVersion 91 | // optimizer: { 92 | // enabled: false, 93 | // runs: 200 94 | // }, 95 | // evmVersion: "byzantium" 96 | // } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /utils/EVMThrow.js: -------------------------------------------------------------------------------- 1 | export default 'VM Exception while processing transaction: revert'; 2 | -------------------------------------------------------------------------------- /utils/asserts.js: -------------------------------------------------------------------------------- 1 | export const assertExpectedError = async (promise) => { 2 | try { 3 | await promise; 4 | throw new Error('Should have failed but did not!'); 5 | } catch (error) { 6 | assert.isTrue(error.message.indexOf('invalid opcode') >= 0, `Unexpected error message: ${error.message}`); 7 | } 8 | }; 9 | 10 | export const assertEvent = async (event, predicate) => new Promise((resolve, reject) => { 11 | event({}, { fromBlock: 0, toBlock: 'latest' }).get((error, logs) => { 12 | try { 13 | assert.isOk(logs, `No event logs found, error: ${error}`); 14 | const actualEvents = logs.filter(predicate); 15 | assert.equal(actualEvents.length, 1, 'No event found'); 16 | resolve(); 17 | } catch (e) { 18 | reject(e); 19 | } 20 | }); 21 | }); 22 | 23 | export const assertTransferEvent = async (contract, from, to, amount) => { 24 | const predicate = ({ args }) => args.from === from && args.to === to && args.value.eq(amount); 25 | await assertEvent(contract.Transfer, predicate); 26 | }; 27 | 28 | export const assertEqual = (a, b) => assert.isTrue(Object.is(a, b), `Expected ${a.toString()} to equal ${b.toString()}`); 29 | export const assertNotEqual = (a, b) => assert.isFalse(Object.is(a, b), `Expected ${a.toString()} to not equal ${b.toString()}`); 30 | export const assertTrue = (a) => assert.isTrue(a, 'Mismatch'); 31 | export const assertFalse = (a) => assert.isFalse(a, 'Mismatch'); 32 | -------------------------------------------------------------------------------- /utils/ether.js: -------------------------------------------------------------------------------- 1 | export const ether = (n) => new web3.BigNumber(web3.toWei(n, 'ether')); 2 | 3 | export const oneEther = ether(1); 4 | 5 | export const transaction = (address, wei) => ({ 6 | from: address, 7 | value: wei, 8 | }); 9 | 10 | export const ethBalance = (address) => web3.eth.getBalance(address); 11 | -------------------------------------------------------------------------------- /utils/index.js: -------------------------------------------------------------------------------- 1 | export { default as EVMThrow } from './EVMThrow'; 2 | export { default as timeController } from './timeController'; 3 | export * from './ether'; 4 | export * from './asserts'; 5 | 6 | const { BigNumber } = web3; 7 | require('chai') 8 | .use(require('chai-as-promised')) 9 | .use(require('chai-bignumber')(BigNumber)) 10 | .should(); 11 | 12 | export const duration = { 13 | seconds(val) { return val; }, 14 | minutes(val) { return val * this.seconds(60); }, 15 | hours(val) { return val * this.minutes(60); }, 16 | days(val) { return val * this.hours(24); }, 17 | weeks(val) { return val * this.days(7); }, 18 | years(val) { return val * this.days(365); }, 19 | }; 20 | -------------------------------------------------------------------------------- /utils/timeController.js: -------------------------------------------------------------------------------- 1 | export default (() => { 2 | const mineBlock = () => ( 3 | new Promise((resolve, reject) => web3.currentProvider.sendAsync({ 4 | jsonrpc: '2.0', 5 | method: 'evm_mine', 6 | id: new Date().getTime(), 7 | }, (error, result) => (error ? reject(error) : resolve(result.result)))) 8 | ); 9 | 10 | const addSeconds = (seconds) => ( 11 | new Promise((resolve, reject) => web3.currentProvider.sendAsync({ 12 | jsonrpc: '2.0', 13 | method: 'evm_increaseTime', 14 | params: [seconds], 15 | id: new Date().getTime(), 16 | }, (error, result) => (error ? reject(error) : resolve(result.result)))) 17 | .then(mineBlock) 18 | ); 19 | 20 | const addDays = (days) => addSeconds(days * 24 * 60 * 60); 21 | 22 | const currentTimestamp = () => ( 23 | new web3.BigNumber(web3.eth.getBlock(web3.eth.blockNumber).timestamp) 24 | ); 25 | 26 | function latestTime() { 27 | return web3.eth.getBlock('latest').timestamp; 28 | } 29 | function increaseTimeTo(target) { 30 | const now = latestTime(); 31 | if (target < now) throw Error(`Cannot increase current time(${now}) to a moment in the past(${target})`); 32 | const diff = target - now; 33 | return addSeconds(diff); 34 | } 35 | 36 | 37 | return { 38 | addSeconds, 39 | addDays, 40 | currentTimestamp, 41 | increaseTimeTo, 42 | latestTime, 43 | }; 44 | })(); 45 | --------------------------------------------------------------------------------