//@dev reduce gas costs on takeLoan() /* ___ ___ _ _____ _ ______ ___________ | \/ | (_) |_ _| | | | ___ \____ | _ \ | . . | __ _ _ __ __ _ _ _ __ | |_ __ __ _ __| | ___ _ __ | |_/ / / / | | | | |\/| |/ _` | '__/ _` | | '_ \ | | '__/ _` |/ _` |/ _ \ '__|| __/ \ \ | | | | | | | (_| | | | (_| | | | | | | | | | (_| | (_| | __/ | | | .___/ / |/ / \_| |_/\__,_|_| \__, |_|_| |_| \_/_| \__,_|\__,_|\___|_| \_| \____/|___/ __/ | |___/ _____________________ | _________________ | | | / | | | | /\ / | | | | /\ / \ / | | | | / \/ \/ | | | |/ P3D | | | |_________________| | | __ __ __ __ __ __ | | |__|__|__|__|__|__| | | |__|__|__|__|__|__| | | |__|__|__|__|__|__| | | |__|__|__|__|__|__| | | |__|__|__|__|__|__| | | |__|__|__|__|__|__| | | ___ ___ ___ ___ | | | 7 | 8 | 9 | | + | | | |___|___|___| |___| | | | 4 | 5 | 6 | | - | | | |___|___|___| |___| | | | 1 | 2 | 3 | | x | | | |___|___|___| |___| | | | . | 0 | = | | / | | | |___|___|___| |___| | |_____________________| //@dev instructions go here by nightman */ pragma solidity 0.4.25; //////////////// //P3D ABSTRACT// //////////////// contract HourglassAbstract { function stakingRequirement() pure public returns(uint256) {} function buyPrice() pure public returns(uint256) {} function balanceOf(address) pure public returns(uint256) {} //user function dividendsOf(address) pure public returns(uint256) {} //user function calculateEthereumReceived(uint256) pure public returns(uint256) {} //tokens function buy(address) public payable returns(uint256) {} //referral function reinvest() pure public {} function withdraw() pure public {} function exit() pure public {} } //////////////////// //PRIMARY CONTRACT// //////////////////// contract MarginLongP3D { using SafeMath for *; /////////// //STORAGE// /////////// address public admin; uint256 public adminBalance; address public shortsContract; bool public shortsContractSet; address public p3d = 0xB3775fB83F7D12A36E0475aBdD1FCA35c091efBe; uint32 public loanOfferId; uint32 public activeLoanId; uint32 public minLoanDailyInterestRate = 0; uint32 public maxLoanDailyInterestRate = 10000; //100% per day uint64 public minLoanDuration = 24 hours; uint64 public maxLoanDuration = 365 days; uint128 public minLoanOfferAmount = 0.5 ether; //raised to prevent annoying spam loan listings uint256 public maxLeverage = 4; //400% of collateral + collateral for 5x leverage (eg. 4 ETH can borrow up to 16 ETH for 20 ETH of purchasing power) uint256 public maintainanceMargin = 10; //positions are required to maintain at least 110% of initial loan or risk liquidation (dynamic with interest) mapping (uint256 => LoanOffer) public loanOffers; mapping (uint256 => ActiveLoan) internal activeLoans; //nested structs won't display automatically so there is a separate view function mapping (address => uint256[]) public offersByAddress; mapping (address => uint256[]) public activePositionsByAddress; mapping (address => uint256) public userBalance; uint256 public activeProxyContracts; mapping (address => bool) public proxyWhitelist; mapping (uint256 => address) public activeProxies; /////////////// //CONSTRUCTOR// /////////////// constructor() public payable{ admin = msg.sender; // @dev buy masternode here? // require(msg.value >= calcMasternodeCost(), "you must give enough for a masternode"); // adminBuyProxy(); } ///////////// //MODIFIERS// ///////////// modifier onlyAdmin() { require (admin == msg.sender, "you must be the admin"); _; } modifier onlyWhitelist() { require (proxyWhitelist[msg.sender] == true, "only whitelisted contracts allowed"); _; } ////////// //EVENTS// ////////// event logAdminWithdrawal( uint256 amount_, address indexed user_, string message_ ); event logDonation( uint256 amount_, address indexed sender_, string message_ ); event logAdminBuy( uint256 amount_, address indexed sender_, string message_ ); event logAdminReinvest( address indexed sender_, string message_ ); event logAdminProxyWithdrawal( address indexed sender_, string message_ ); event logAdminExit( address indexed sender_, string message_ ); event logNewLoanOffer( uint32 indexed id_, uint32 dailyInterestRate_, uint64 duration_, uint128 amount_, address indexed lender_, string message_ ); event logLoanOfferRemoved( uint32 indexed id_, address indexed lender_, string message_ ); event logLoanTaken( uint256 indexed activeLoanId_, uint256 loanOfferId_, uint256 started_, uint256 expiry_, uint256 liquidationPrice_, address indexed borrower_, address indexed proxy_, string message_ ); event logUserReinvest( address indexed borrower_, address indexed proxy_, uint256 indexed activeLoanId_, string message_ ); event logUserExit( address indexed borrower_, address indexed proxy_, uint256 indexed activeLoanId_, uint256 fundsReceived_, string message_ ); event logLiquidation( address indexed liquidator_, address indexed proxy_, uint256 indexed activeLoanId_, uint256 fundsReceived_, string message_ ); event logUserWithdrawal( uint256 amount_, address indexed user_, string message_ ); //@dev temp for debugging event debug(string message); /////////// //STRUCTS// /////////// struct LoanOffer { //tightly packed bits to save gas uint32 id; //max 4294967295 uint32 dailyInterestRate; //max 4294967295, precise to two decimals (eg 387 is 3.78% per day) uint64 duration; //max 18446744073709551615 uint128 amount; //max uint128 is greater than total ETH supply address lender; bool filled; } struct ActiveLoan { //tightly packed bits to save gas uint32 id; uint64 started; uint64 expiry; uint96 liquidationPrice; //max uint96 is greater than total ETH supply bool closed; address borrower; address proxy; LoanOffer loanOffer; } ////////////////////////// //CONTRACT INSTANTIATION// ////////////////////////// HourglassAbstract h = HourglassAbstract(p3d); /////////////////// //ADMIN FUNCTIONS// /////////////////// //set short contract address //can only be called once for trustlessness function setShortsContractAddress(address _address) external onlyAdmin() { require (shortsContractSet == false, "you can only set the address once"); shortsContract = _address; shortsContractSet = true; } //admin withdraw function adminWithdraw() external onlyAdmin() { require (adminBalance > 0, "you must have a balance to withdraw"); //update accounting uint256 balance = adminBalance; adminBalance = 0; //transfer admin.transfer(balance); emit logAdminWithdrawal(balance, admin, "the admin just withdrew"); } //admin buy in to ensure there is enough for a masternode function adminBuyProxy() public payable onlyAdmin() { h.buy.value(msg.value)(address(this)); emit logAdminBuy(msg.value, msg.sender, "the admin just bought"); } //other admin proxy functions function adminReinvestProxy() external onlyAdmin() { h.reinvest(); emit logAdminReinvest(msg.sender, "the admin just reinvested"); } function adminWithdrawProxy() external onlyAdmin() { h.withdraw(); emit logAdminProxyWithdrawal(msg.sender, "the admin just withdrew via proxy"); } function adminExitProxy() external onlyAdmin() { h.exit(); emit logAdminExit(msg.sender, "the admin just exited"); } ////////////////// //USER FUNCTIONS// ////////////////// //takes in money from masternodes or random ether sent to contract function() external payable { require (msg.value > 0, "fallback function only accepts ether"); //log balance adminBalance += msg.value; emit logDonation(msg.value, msg.sender, "thanks for the donation"); } //called by lenders to list loans function listLoan(uint32 _dailyInterestRate, uint64 _duration, uint128 _amount) external payable { require (loanOfferId < 4294967295, "uint8 full. time to make a new contract"); require (_amount == msg.value && _amount > minLoanOfferAmount, "you must provide enough capital for this loan offer"); require (_dailyInterestRate >= minLoanDailyInterestRate && _dailyInterestRate < maxLoanDailyInterestRate, "the interest rate must be less than 100% a day"); require (_duration >= minLoanDuration && _duration < maxLoanDuration, "the duration must be longer than one day and less than one year"); //increment id (first loan starts at 1) loanOfferId++; //instantiate struct in storage LoanOffer storage l = loanOffers[loanOfferId]; //update values l.id = loanOfferId; l.dailyInterestRate = _dailyInterestRate; //precise to two decimals (eg 175 is 1.75% per day; 1 is 0.01% a day) l.duration = _duration; l.amount = _amount; l.lender = msg.sender; l.filled = false; //update accounting offersByAddress[msg.sender].push(loanOfferId); emit logNewLoanOffer(loanOfferId, _dailyInterestRate, _duration, _amount, msg.sender, "new loan offer"); } //called by lenders to remove open listings function removeListing(uint32 _id) external { require (msg.sender == loanOffers[_id].lender, "you must be the lender of this loan"); require (loanOffers[_id].filled == false, "the loan offer must be open"); //update struct tracking loanOffers[_id].filled = true; uint256 balance = loanOffers[_id].amount; loanOffers[_id].amount = 0; //return full amount to user balance userBalance[msg.sender] += balance; emit logLoanOfferRemoved(_id, msg.sender, "loan offer removed"); } //user withdraw function userWithdraw() external { require (userBalance[msg.sender] > 0, "you must have a balance to withdraw"); //update accounting uint256 balance = userBalance[msg.sender]; userBalance[msg.sender] = 0; //if a user bought in with a contract or otherwise does not accept this transfer it's their problem msg.sender.transfer(balance); emit logUserWithdrawal(balance, msg.sender, "a user withdrew their balance"); } //called by borrower to take on an open loan offer function takeLoan(uint32 _id) external payable { //prevent overflow of id tracker require (activeLoanId < 4294967295, "uint8 full. time to make a new contract"); require (msg.value.mul(maxLeverage) >= loanOffers[_id].amount, "you must provide sufficient collateral"); require (loanOffers[_id].filled == false, "the loan offer must be open"); //update loan offer loanOffers[_id].filled = true; //increment ids activeProxyContracts++; activeLoanId++; //update accounting uint256 position = msg.value + loanOffers[_id].amount; //launch new contract //note: it would be nice to use CREATE2 for this after Constantinople ProxyBuyer p = (new ProxyBuyer).value(position)(activeLoanId, msg.sender); proxyWhitelist[p] = true; activeProxies[activeLoanId] = p; //update active loan struct ActiveLoan storage a = activeLoans[activeLoanId]; //@dev am I actually saving gas with all of these uint bit conversions? a.id = activeLoanId; a.started = uint64(now); a.expiry = uint64(now.add(loanOffers[_id].duration)); a.liquidationPrice = uint96(msg.value.add(msg.value.mul(maintainanceMargin).div(100))); a.closed = false; a.borrower = msg.sender; a.proxy = p; a.loanOffer = loanOffers[_id]; //update accounting activePositionsByAddress[msg.sender].push(activeLoanId); emit logLoanTaken(activeLoanId, _id, now, a.expiry, a.liquidationPrice, msg.sender, p, "a loan was taken out"); } //can be called by borrower to reinvest function reinvestPosition(uint32 _activeLoanId) external { require (msg.sender == activeLoans[_activeLoanId].borrower, "you must own this position"); require (activeLoans[_activeLoanId].closed == false, "the position must be active"); //note: not checking to see if reinvest puts the position into at risk state. that is the user's responsibility require (isPositionAtRisk(_activeLoanId) == false, "the position cannot be at risk"); //local contract instantiation ProxyBuyer p = ProxyBuyer(activeProxies[_activeLoanId]); p.reinvestProxy(); emit logUserReinvest(msg.sender, p, _activeLoanId, "a user reinvested"); } //called by borrower to close position //borrower can call this even if the contract is at risk, giving them a better chance at some return function exitPosition(uint32 _activeLoanId) external { require (msg.sender == activeLoans[_activeLoanId].borrower, "you must own this position"); require (activeLoans[_activeLoanId].closed == false, "the position must be active"); //local contract instantiation ProxyBuyer p = ProxyBuyer(activeProxies[_activeLoanId]); p.exitProxyClose(msg.sender); } //called by liquidators to dissolve risky or expired positions function liquidatePosition(uint32 _activeLoanId) external { require (activeLoans[_activeLoanId].closed == false, "the position must be active"); require (isPositionAtRisk(_activeLoanId) == true || now > activeLoans[_activeLoanId].expiry, "the position must be at risk or expired"); //local contract instantiation ProxyBuyer p = ProxyBuyer(activeProxies[_activeLoanId]); p.exitProxyLiquidate(msg.sender); } //log closed proxy return function handleProxyReturn(uint32 _activeLoanId, address _user) public payable onlyWhitelist() { emit debug("handle proxy return called"); //@dev remove after testing //update struct accounting activeProxyContracts--; activeLoans[_activeLoanId].closed = true; uint256 fundsReceived = msg.value; //calculate what lender is owed uint256 lenderOwed = calculateInterestAccrued(_activeLoanId) + activeLoans[_activeLoanId].loanOffer.amount; if (_user == activeLoans[_activeLoanId].borrower) { uint256 borrowerProfit; //return funds to lender if (fundsReceived > lenderOwed) { //the lender can be repaid in full //the borrower gets the rest borrowerProfit = fundsReceived.sub(lenderOwed); userBalance[activeLoans[_activeLoanId].loanOffer.lender] += lenderOwed; userBalance[activeLoans[_activeLoanId].borrower] += borrowerProfit; emit logUserExit(_user, activeProxies[_activeLoanId], _activeLoanId, fundsReceived, "a user closed a position and received funds"); } else { //the lender cannot be repaid in full or can recover 100% of owed funds if fundsReceived == lenderOwed //the borrower gets nothing userBalance[activeLoans[_activeLoanId].loanOffer.lender] += fundsReceived; emit logUserExit(_user, activeProxies[_activeLoanId], _activeLoanId, fundsReceived, "a user closed a position and did not receive funds"); } } else { //the liquidator receives their cut first to incentivize fast liquidations of risky positions //the lender gets up to what they are owed //the borrower gets nothing //any excess is forwarded to the shorts contract or the admin //note: there should only be excess if there are price fluctuations between the transaction is sent and when it confirms uint256 liquidatorProfit = activeLoans[_activeLoanId].liquidationPrice.sub(activeLoans[_activeLoanId].loanOffer.amount); uint256 lenderProfit = fundsReceived.sub(liquidatorProfit); uint256 excessProfit; if (lenderProfit > lenderOwed) { excessProfit = lenderProfit.sub(lenderOwed); userBalance[activeLoans[_activeLoanId].loanOffer.lender] += lenderOwed; } else { userBalance[activeLoans[_activeLoanId].loanOffer.lender] += lenderProfit; } if (shortsContractSet == true) { userBalance[_user] += liquidatorProfit.div(2); //5% of initial loan value to liquidator shortsContract.transfer(liquidatorProfit.div(2).add(excessProfit)); //5% of initial loan value to shorts contract, plus excess } else { userBalance[_user] = liquidatorProfit; //10% of initial loan value to liquidator adminBalance += excessProfit; //admin gets any excess profit in event the shorts contract is not set } emit logLiquidation(_user, activeProxies[_activeLoanId], _activeLoanId, fundsReceived, "a position was liquidated"); } } ////////////////// //VIEW FUNCTIONS// ////////////////// function contractBalance() external view returns(uint256 _balance) { return address(this).balance; } //view active loans without nested struct function viewActiveLoan(uint32 _activeLoanId) external view returns( uint32 _id, uint64 _expiry, uint256 _liquidationPrice, bool _closed, address _borrower, address _proxy, uint256 _loanOfferID ) { return ( activeLoans[_activeLoanId].id, activeLoans[_activeLoanId].expiry, checkLiquidationPrice(_activeLoanId), activeLoans[_activeLoanId].closed, activeLoans[_activeLoanId].borrower, activeLoans[_activeLoanId].proxy, activeLoans[_activeLoanId].loanOffer.id ); } //check if the position is at risk function isPositionAtRisk(uint32 _activeLoanId) public view returns(bool _isAtRisk) { if (checkPositionValue(_activeLoanId) <= checkLiquidationPrice(_activeLoanId)) { return true; } else { return false; } } //check current value of a position function checkPositionValue(uint32 _activeLoanId) public view returns(uint256 _valueInWei) { address address_ = activeProxies[_activeLoanId]; uint256 tokens = h.balanceOf(address_); return h.dividendsOf(address_) + h.calculateEthereumReceived(tokens); } //check tokens held by position function checkPositionTokens(uint32 _activeLoanId) external view returns(uint256 _tokens) { address address_ = activeProxies[_activeLoanId]; return h.balanceOf(address_); } //check liquidation price function checkLiquidationPrice(uint32 _activeLoanId) public view returns(uint256 _price) { return activeLoans[_activeLoanId].liquidationPrice + calculateInterestAccrued(_activeLoanId); } //calculate interest accrued up to the minute //precise to 0.00000001% function calculateInterestAccrued(uint32 _activeLoanId) public view returns(uint256 _interest) { if (activeLoans[_activeLoanId].loanOffer.dailyInterestRate == 0) { return 0; } else { uint256 minutesActive = (now.sub(activeLoans[_activeLoanId].started)).div(60); uint256 interestPerMinute = activeLoans[_activeLoanId].loanOffer.dailyInterestRate.mul(100000).div(1440); uint256 totalRate = minutesActive.mul(interestPerMinute); uint256 interestOwed = activeLoans[_activeLoanId].loanOffer.amount.mul(totalRate).div(1000000000); return interestOwed; } } function calcMasternodeCost() public view returns(uint256 _masternodePrice) { return h.stakingRequirement().mul(h.buyPrice().div(10**18)); } //get number of offers or positions by user address function getUserLoanOfferCount(address _address) external view returns(uint256 _count) { return offersByAddress[_address].length; } function getUserPositionCount(address _address) external view returns(uint256 _count) { return activePositionsByAddress[_address].length; } //unnecessarily convoluted functions to get solidity to return values stored in a mapping of an array //set up as bounded loop in case user has large quantity of entries function getUserLoanOfferIDs(address _address, uint256 _indexStart, uint256 _indexEnd) external view returns(uint256[] _loanOfferIDs) { require (offersByAddress[_address].length > 0, "the user must have at least one offer"); require (_indexStart < _indexEnd, "you must provide a valid index range"); require (_indexEnd < offersByAddress[_address].length, "you must provide an end index that exists"); //instantiate temp memory array uint256 length = _indexEnd - _indexStart + 1; uint256[] memory memArray = new uint256[](length); //loop through values in index range uint256 j = 0; for (uint256 i = _indexStart; i <= _indexEnd; i++) { memArray[j] = offersByAddress[_address][i]; j++; } return memArray; } function getUserPositionIDs(address _address, uint256 _indexStart, uint256 _indexEnd) external view returns(uint256[] _activePositionIDs) { require (activePositionsByAddress[_address].length > 0, "the user must have at least one position"); require (_indexStart < _indexEnd, "you must provide a valid index range"); require (_indexEnd < activePositionsByAddress[_address].length, "you must provide an end index that exists"); //instantiate temp memory array uint256 length = _indexEnd - _indexStart + 1; uint256[] memory memArray = new uint256[](length); //loop through values in index range uint256 j = 0; for (uint256 i = _indexStart; i <= _indexEnd; i++) { memArray[j] = activePositionsByAddress[_address][i]; j++; } return memArray; } } //////////////////////////////////// //MAIN CONTRACT ABSTRACT FOR PROXY// //////////////////////////////////// contract MarginTradingAbstract { function handleProxyReturn(uint32, address) public payable {} } ////////////////// //PROXY CONTRACT// ////////////////// contract ProxyBuyer is HourglassAbstract, MarginTradingAbstract { uint32 public id; address public p3d = 0xB3775fB83F7D12A36E0475aBdD1FCA35c091efBe; address public mainContract; address public user; address public lastCaller; constructor(uint32 _id, address _user) public payable { mainContract = msg.sender; //lastCaller set as mainContract so funds are not accidentally attributed to 0x0 lastCaller = msg.sender; user = _user; id = _id; emit debug("new contract depolyed"); //@dev remove after testing buyProxy(); } //@dev temp for debugging event debug(string message); modifier onlyMainContract() { require (msg.sender == mainContract, "only the main contract can call a proxy"); _; } HourglassAbstract h = HourglassAbstract(p3d); MarginTradingAbstract m = MarginTradingAbstract(mainContract); function buyProxy() public payable onlyMainContract() { h.buy.value(msg.value)(mainContract); emit debug("proxy contract buy"); //@dev remove after testing } function reinvestProxy() external view onlyMainContract() { h.reinvest(); } function exitProxyClose(address _caller) external onlyMainContract() { h.exit(); //sets last caller as borrower lastCaller = _caller; emit debug("proxy contract exit called"); //@dev remove after testing forwardFunds(); } function exitProxyLiquidate(address _caller) external onlyMainContract() { h.exit(); //sets last caller as liquidator lastCaller = _caller; forwardFunds(); } function forwardFunds() internal { m.handleProxyReturn.value(address(this).balance)(id, lastCaller); emit debug("forward funds called"); //@dev remove after testing } function() public payable { if (msg.sender != p3d) { //thanks for the donations mainContract.transfer(msg.value); } } } ///////////// //LIBRARIES// ///////////// library SafeMath { function mul(uint256 a, uint256 b) internal pure returns(uint256) { if (a == 0) { return 0; } uint256 c = a * b; require (c / a == b, "the SafeMath multiplication check failed"); return c; } function div(uint256 a, uint256 b) internal pure returns(uint256) { require (b > 0, "the SafeMath division check failed"); uint256 c = a / b; return c; } function sub(uint256 a, uint256 b) internal pure returns(uint256) { require (b <= a, "the SafeMath subtraction check failed"); return a - b; } function add(uint256 a, uint256 b) internal pure returns(uint256) { uint256 c = a + b; require (c >= a, "the SafeMath addition check failed"); return c; } function mod(uint256 a, uint256 b) internal pure returns(uint256) { require (b != 0, "the SafeMath modulo check failed"); return a % b; } }
0.4.25