Created
August 22, 2018 11:52
-
-
Save Ramarti/e2b67ba37dea3f448e51b5ff206f16c5 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
pragma solidity ^0.4.23; | |
/** | |
* @title SafeMath | |
* @dev Math operations with safety checks that throw on error | |
*/ | |
library SafeMath { | |
/** | |
* @dev Multiplies two numbers, throws on overflow. | |
*/ | |
function mul(uint256 a, uint256 b) internal pure returns (uint256 c) { | |
// Gas optimization: this is cheaper than asserting 'a' not being zero, but the | |
// benefit is lost if 'b' is also tested. | |
// See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522 | |
if (a == 0) { | |
return 0; | |
} | |
c = a * b; | |
assert(c / a == b); | |
return c; | |
} | |
/** | |
* @dev Integer division of two numbers, truncating the quotient. | |
*/ | |
function div(uint256 a, uint256 b) internal pure returns (uint256) { | |
// assert(b > 0); // Solidity automatically throws when dividing by 0 | |
// uint256 c = a / b; | |
// assert(a == b * c + a % b); // There is no case in which this doesn't hold | |
return a / b; | |
} | |
/** | |
* @dev Subtracts two numbers, throws on overflow (i.e. if subtrahend is greater than minuend). | |
*/ | |
function sub(uint256 a, uint256 b) internal pure returns (uint256) { | |
assert(b <= a); | |
return a - b; | |
} | |
/** | |
* @dev Adds two numbers, throws on overflow. | |
*/ | |
function add(uint256 a, uint256 b) internal pure returns (uint256 c) { | |
c = a + b; | |
assert(c >= a); | |
return c; | |
} | |
} | |
/** | |
* @title Ownable | |
* @dev The Ownable contract has an owner address, and provides basic authorization control | |
* functions, this simplifies the implementation of "user permissions". | |
*/ | |
contract Ownable { | |
address public owner; | |
event OwnershipRenounced(address indexed previousOwner); | |
event OwnershipTransferred( | |
address indexed previousOwner, | |
address indexed newOwner | |
); | |
/** | |
* @dev The Ownable constructor sets the original `owner` of the contract to the sender | |
* account. | |
*/ | |
constructor() public { | |
owner = msg.sender; | |
} | |
/** | |
* @dev Throws if called by any account other than the owner. | |
*/ | |
modifier onlyOwner() { | |
require(msg.sender == owner); | |
_; | |
} | |
/** | |
* @dev Allows the current owner to relinquish control of the contract. | |
*/ | |
function renounceOwnership() public onlyOwner { | |
emit OwnershipRenounced(owner); | |
owner = address(0); | |
} | |
/** | |
* @dev Allows the current owner to transfer control of the contract to a newOwner. | |
* @param _newOwner The address to transfer ownership to. | |
*/ | |
function transferOwnership(address _newOwner) public onlyOwner { | |
_transferOwnership(_newOwner); | |
} | |
/** | |
* @dev Transfers control of the contract to a newOwner. | |
* @param _newOwner The address to transfer ownership to. | |
*/ | |
function _transferOwnership(address _newOwner) internal { | |
require(_newOwner != address(0)); | |
emit OwnershipTransferred(owner, _newOwner); | |
owner = _newOwner; | |
} | |
} | |
/** | |
* @title Pausable | |
* @dev Base contract which allows children to implement an emergency stop mechanism. | |
*/ | |
contract Pausable is Ownable { | |
event Pause(); | |
event Unpause(); | |
bool public paused = false; | |
/** | |
* @dev Modifier to make a function callable only when the contract is not paused. | |
*/ | |
modifier whenNotPaused() { | |
require(!paused); | |
_; | |
} | |
/** | |
* @dev Modifier to make a function callable only when the contract is paused. | |
*/ | |
modifier whenPaused() { | |
require(paused); | |
_; | |
} | |
/** | |
* @dev called by the owner to pause, triggers stopped state | |
*/ | |
function pause() onlyOwner whenNotPaused public { | |
paused = true; | |
emit Pause(); | |
} | |
/** | |
* @dev called by the owner to unpause, returns to normal state | |
*/ | |
function unpause() onlyOwner whenPaused public { | |
paused = false; | |
emit Unpause(); | |
} | |
} | |
contract EthicHubReputationInterface { | |
modifier onlyUsersContract(){_;} | |
modifier onlyLendingContract(){_;} | |
function burnReputation(uint delayDays) external; | |
function incrementReputation(uint completedProjectsByTier) external; | |
function initLocalNodeReputation(address localNode) external; | |
function initCommunityReputation(address community) external; | |
function getCommunityReputation(address target) public view returns(uint256); | |
function getLocalNodeReputation(address target) public view returns(uint256); | |
} | |
contract EthicHubStorageInterface { | |
//modifier for access in sets and deletes | |
modifier onlyEthicHubContracts() {_;} | |
// Setters | |
function setAddress(bytes32 _key, address _value) external; | |
function setUint(bytes32 _key, uint _value) external; | |
function setString(bytes32 _key, string _value) external; | |
function setBytes(bytes32 _key, bytes _value) external; | |
function setBool(bytes32 _key, bool _value) external; | |
function setInt(bytes32 _key, int _value) external; | |
// Deleters | |
function deleteAddress(bytes32 _key) external; | |
function deleteUint(bytes32 _key) external; | |
function deleteString(bytes32 _key) external; | |
function deleteBytes(bytes32 _key) external; | |
function deleteBool(bytes32 _key) external; | |
function deleteInt(bytes32 _key) external; | |
// Getters | |
function getAddress(bytes32 _key) external view returns (address); | |
function getUint(bytes32 _key) external view returns (uint); | |
function getString(bytes32 _key) external view returns (string); | |
function getBytes(bytes32 _key) external view returns (bytes); | |
function getBool(bytes32 _key) external view returns (bool); | |
function getInt(bytes32 _key) external view returns (int); | |
} | |
contract EthicHubBase { | |
uint8 public version; | |
EthicHubStorageInterface public ethicHubStorage = EthicHubStorageInterface(0); | |
constructor(address _storageAddress) public { | |
require(_storageAddress != address(0)); | |
ethicHubStorage = EthicHubStorageInterface(_storageAddress); | |
} | |
} | |
contract EthicHubLending is EthicHubBase, Ownable, Pausable { | |
using SafeMath for uint256; | |
uint256 public minContribAmount = 0.1 ether; // 0.01 ether | |
enum LendingState { | |
Uninitialized, | |
AcceptingContributions, | |
ExchangingToFiat, | |
AwaitingReturn, | |
ProjectNotFunded, | |
ContributionReturned, | |
Default | |
} | |
mapping(address => Investor) public investors; | |
uint256 public investorCount; | |
uint256 public fundingStartTime; // Start time of contribution period in UNIX time | |
uint256 public fundingEndTime; // End time of contribution period in UNIX time | |
uint256 public totalContributed; | |
bool public capReached; | |
LendingState public state; | |
uint256 public annualInterest; | |
uint256 public totalLendingAmount; | |
uint256 public lendingDays; | |
uint256 public initialEthPerFiatRate; | |
uint256 public totalLendingFiatAmount; | |
address public borrower; | |
address public localNode; | |
address public ethicHubTeam; | |
uint256 public borrowerReturnDate; | |
uint256 public borrowerReturnEthPerFiatRate; | |
uint256 public constant ethichubFee = 3; | |
uint256 public constant localNodeFee = 4; | |
uint256 public tier; | |
// interest rate is using base uint 100 and 100% 10000, this means 1% is 100 | |
// this guarantee we can have a 2 decimal presicion in our calculation | |
uint256 public constant interestBaseUint = 100; | |
uint256 public constant interestBasePercent = 10000; | |
bool public localNodeFeeReclaimed; | |
bool public ethicHubTeamFeeReclaimed; | |
uint256 public surplusEth; | |
uint256 public returnedEth; | |
struct Investor { | |
uint256 amount; | |
bool isCompensated; | |
bool surplusEthReclaimed; | |
} | |
// events | |
event onCapReached(uint endTime); | |
event onContribution(uint totalContributed, address indexed investor, uint amount, uint investorsCount); | |
event onCompensated(address indexed contributor, uint amount); | |
event onSurplusSent(uint256 amount); | |
event onSurplusReclaimed(address indexed contributor, uint amount); | |
event StateChange(uint state); | |
event onInitalRateSet(uint rate); | |
event onReturnRateSet(uint rate); | |
event onReturnAmount(address indexed borrower, uint amount); | |
event onBorrowerChanged(address indexed newBorrower); | |
// modifiers | |
modifier checkProfileRegistered(string profile) { | |
bool isRegistered = ethicHubStorage.getBool(keccak256("user", profile, msg.sender)); | |
require(isRegistered); | |
_; | |
} | |
modifier checkIfArbiter() { | |
address arbiter = ethicHubStorage.getAddress(keccak256("arbiter", this)); | |
require(arbiter == msg.sender); | |
_; | |
} | |
modifier onlyOwnerOrLocalNode() { | |
require(localNode == msg.sender || owner == msg.sender); | |
_; | |
} | |
constructor( | |
uint256 _fundingStartTime, | |
uint256 _fundingEndTime, | |
address _borrower, | |
uint256 _annualInterest, | |
uint256 _totalLendingAmount, | |
uint256 _lendingDays, | |
address _storageAddress, | |
address _localNode, | |
address _ethicHubTeam | |
) | |
EthicHubBase(_storageAddress) | |
public { | |
require(_fundingStartTime > now); | |
require(_fundingEndTime > fundingStartTime); | |
require(_borrower != address(0)); | |
require(ethicHubStorage.getBool(keccak256("user", "representative", _borrower))); | |
require(_localNode != address(0)); | |
require(_ethicHubTeam != address(0)); | |
require(ethicHubStorage.getBool(keccak256("user", "localNode", _localNode))); | |
require(_totalLendingAmount > 0); | |
require(_lendingDays > 0); | |
require(_annualInterest > 0 && _annualInterest < 100); | |
version = 2; | |
fundingStartTime = _fundingStartTime; | |
fundingEndTime = _fundingEndTime; | |
localNode = _localNode; | |
ethicHubTeam = _ethicHubTeam; | |
borrower = _borrower; | |
annualInterest = _annualInterest; | |
totalLendingAmount = _totalLendingAmount; | |
lendingDays = _lendingDays; | |
state = LendingState.Uninitialized; | |
} | |
function saveInitialParametersToStorage(uint256 _maxDelayDays, uint256 _tier, uint256 _communityMembers, address _community) external onlyOwnerOrLocalNode { | |
require(_maxDelayDays != 0); | |
require(state == LendingState.Uninitialized); | |
require(_tier > 0); | |
require(_communityMembers > 0); | |
require(ethicHubStorage.getBool(keccak256("user", "community", _community))); | |
ethicHubStorage.setUint(keccak256("lending.maxDelayDays", this), _maxDelayDays); | |
ethicHubStorage.setAddress(keccak256("lending.community", this), _community); | |
ethicHubStorage.setAddress(keccak256("lending.localNode", this), localNode); | |
ethicHubStorage.setUint(keccak256("lending.tier", this), _tier); | |
ethicHubStorage.setUint(keccak256("lending.communityMembers", this), _communityMembers); | |
tier = _tier; | |
state = LendingState.AcceptingContributions; | |
emit StateChange(uint(state)); | |
} | |
function setBorrower(address _borrower) external checkIfArbiter { | |
require(_borrower != address(0)); | |
require(ethicHubStorage.getBool(keccak256("user", "representative", _borrower))); | |
borrower = _borrower; | |
emit onBorrowerChanged(borrower); | |
} | |
function() public payable whenNotPaused { | |
require(state == LendingState.AwaitingReturn || state == LendingState.AcceptingContributions || state == LendingState.ExchangingToFiat); | |
if(state == LendingState.AwaitingReturn) { | |
returnBorrowedEth(); | |
} else if (state == LendingState.ExchangingToFiat) { | |
// borrower can send surplus eth back to contract to avoid paying interest | |
sendBackSurplusEth(); | |
} else { | |
contributeWithAddress(msg.sender); | |
} | |
} | |
function sendBackSurplusEth() internal { | |
require(state == LendingState.ExchangingToFiat); | |
require(msg.sender == borrower); | |
surplusEth = surplusEth.add(msg.value); | |
require(surplusEth <= totalLendingAmount); | |
emit onSurplusSent(msg.value); | |
} | |
/** | |
* After the contribution period ends unsuccesfully, this method enables the contributor | |
* to retrieve their contribution | |
*/ | |
function declareProjectNotFunded() external onlyOwnerOrLocalNode { | |
require(totalContributed < totalLendingAmount); | |
require(state == LendingState.AcceptingContributions); | |
require(now > fundingEndTime); | |
state = LendingState.ProjectNotFunded; | |
emit StateChange(uint(state)); | |
} | |
function declareProjectDefault() external onlyOwnerOrLocalNode { | |
require(state == LendingState.AwaitingReturn); | |
uint maxDelayDays = getMaxDelayDays(); | |
require(getDelayDays(now) >= maxDelayDays); | |
EthicHubReputationInterface reputation = EthicHubReputationInterface(ethicHubStorage.getAddress(keccak256("contract.name", "reputation"))); | |
require(reputation != address(0)); | |
ethicHubStorage.setUint(keccak256("lending.delayDays", this), maxDelayDays); | |
reputation.burnReputation(maxDelayDays); | |
state = LendingState.Default; | |
emit StateChange(uint(state)); | |
} | |
function setBorrowerReturnEthPerFiatRate(uint256 _borrowerReturnEthPerFiatRate) external onlyOwnerOrLocalNode { | |
require(state == LendingState.AwaitingReturn); | |
borrowerReturnEthPerFiatRate = _borrowerReturnEthPerFiatRate; | |
emit onReturnRateSet(borrowerReturnEthPerFiatRate); | |
} | |
function finishInitialExchangingPeriod(uint256 _initialEthPerFiatRate) external onlyOwnerOrLocalNode { | |
require(capReached == true); | |
require(state == LendingState.ExchangingToFiat); | |
initialEthPerFiatRate = _initialEthPerFiatRate; | |
if (surplusEth > 0) { | |
totalLendingAmount = totalLendingAmount.sub(surplusEth); | |
} | |
totalLendingFiatAmount = totalLendingAmount.mul(initialEthPerFiatRate); | |
emit onInitalRateSet(initialEthPerFiatRate); | |
state = LendingState.AwaitingReturn; | |
emit StateChange(uint(state)); | |
} | |
/** | |
* Method to reclaim contribution after project is declared default (% of partial funds) | |
* @param beneficiary the contributor | |
* | |
*/ | |
function reclaimContributionDefault(address beneficiary) external { | |
require(state == LendingState.Default); | |
require(!investors[beneficiary].isCompensated); | |
// contribution = contribution * partial_funds / total_funds | |
uint256 contribution = checkInvestorReturns(beneficiary); | |
require(contribution > 0); | |
investors[beneficiary].isCompensated = true; | |
beneficiary.transfer(contribution); | |
} | |
/** | |
* Method to reclaim contribution after a project is declared as not funded | |
* @param beneficiary the contributor | |
* | |
*/ | |
function reclaimContribution(address beneficiary) external { | |
require(state == LendingState.ProjectNotFunded); | |
require(!investors[beneficiary].isCompensated); | |
uint256 contribution = investors[beneficiary].amount; | |
require(contribution > 0); | |
investors[beneficiary].isCompensated = true; | |
beneficiary.transfer(contribution); | |
} | |
function reclaimSurplusEth(address beneficiary) external { | |
require(surplusEth > 0); | |
// only can be reclaimed after cap reduced | |
require(state != LendingState.ExchangingToFiat); | |
require(!investors[beneficiary].surplusEthReclaimed); | |
uint256 surplusContribution = investors[beneficiary].amount.mul(surplusEth).div(surplusEth.add(totalLendingAmount)); | |
require(surplusContribution > 0); | |
investors[beneficiary].surplusEthReclaimed = true; | |
emit onSurplusReclaimed(beneficiary, surplusContribution); | |
beneficiary.transfer(surplusContribution); | |
} | |
function reclaimContributionWithInterest(address beneficiary) external { | |
require(state == LendingState.ContributionReturned); | |
require(!investors[beneficiary].isCompensated); | |
uint256 contribution = checkInvestorReturns(beneficiary); | |
require(contribution > 0); | |
investors[beneficiary].isCompensated = true; | |
beneficiary.transfer(contribution); | |
} | |
function reclaimLocalNodeFee() external { | |
require(state == LendingState.ContributionReturned); | |
require(localNodeFeeReclaimed == false); | |
uint256 fee = totalLendingFiatAmount.mul(localNodeFee).mul(interestBaseUint).div(interestBasePercent).div(borrowerReturnEthPerFiatRate); | |
require(fee > 0); | |
localNodeFeeReclaimed = true; | |
localNode.transfer(fee); | |
} | |
function reclaimEthicHubTeamFee() external { | |
require(state == LendingState.ContributionReturned); | |
require(ethicHubTeamFeeReclaimed == false); | |
uint256 fee = totalLendingFiatAmount.mul(ethichubFee).mul(interestBaseUint).div(interestBasePercent).div(borrowerReturnEthPerFiatRate); | |
require(fee > 0); | |
ethicHubTeamFeeReclaimed = true; | |
ethicHubTeam.transfer(fee); | |
} | |
function returnBorrowedEth() internal { | |
require(state == LendingState.AwaitingReturn); | |
require(msg.sender == borrower); | |
require(borrowerReturnEthPerFiatRate > 0); | |
bool projectRepayed = false; | |
uint excessRepayment = 0; | |
uint newReturnedEth = 0; | |
emit onReturnAmount(msg.sender, msg.value); | |
(newReturnedEth, projectRepayed, excessRepayment) = calculatePaymentGoal( | |
borrowerReturnAmount(), | |
returnedEth, | |
msg.value); | |
returnedEth = newReturnedEth; | |
if (projectRepayed == true) { | |
state = LendingState.ContributionReturned; | |
emit StateChange(uint(state)); | |
updateReputation(); | |
} | |
if (excessRepayment > 0) { | |
msg.sender.transfer(excessRepayment); | |
} | |
} | |
// @notice Function to participate in contribution period | |
// Amounts from the same address should be added up | |
// If cap is reached, end time should be modified | |
// Funds should be transferred into multisig wallet | |
// @param contributor Address | |
function contributeWithAddress(address contributor) internal checkProfileRegistered('investor') whenNotPaused { | |
require(state == LendingState.AcceptingContributions); | |
require(msg.value >= minContribAmount); | |
require(isContribPeriodRunning()); | |
uint oldTotalContributed = totalContributed; | |
uint newTotalContributed = 0; | |
uint excessContribValue = 0; | |
(newTotalContributed, capReached, excessContribValue) = calculatePaymentGoal( | |
totalLendingAmount, | |
oldTotalContributed, | |
msg.value); | |
totalContributed = newTotalContributed; | |
if (capReached) { | |
fundingEndTime = now; | |
emit onCapReached(fundingEndTime); | |
} | |
if (investors[contributor].amount == 0) { | |
investorCount = investorCount.add(1); | |
} | |
if (excessContribValue > 0) { | |
msg.sender.transfer(excessContribValue); | |
investors[contributor].amount = investors[contributor].amount.add(msg.value).sub(excessContribValue); | |
emit onContribution(newTotalContributed, contributor, msg.value.sub(excessContribValue), investorCount); | |
} else { | |
investors[contributor].amount = investors[contributor].amount.add(msg.value); | |
emit onContribution(newTotalContributed, contributor, msg.value, investorCount); | |
} | |
} | |
function calculatePaymentGoal(uint goal, uint oldTotal, uint contribValue) internal pure returns(uint, bool, uint) { | |
uint newTotal = oldTotal.add(contribValue); | |
bool goalReached = false; | |
uint excess = 0; | |
if (newTotal >= goal && oldTotal < goal) { | |
goalReached = true; | |
excess = newTotal.sub(goal); | |
contribValue = contribValue.sub(excess); | |
newTotal = goal; | |
} | |
return (newTotal, goalReached, excess); | |
} | |
function sendFundsToBorrower() external onlyOwnerOrLocalNode { | |
//Waiting for Exchange | |
require(state == LendingState.AcceptingContributions); | |
require(capReached); | |
state = LendingState.ExchangingToFiat; | |
emit StateChange(uint(state)); | |
borrower.transfer(totalContributed); | |
} | |
function updateReputation() internal { | |
uint delayDays = getDelayDays(now); | |
EthicHubReputationInterface reputation = EthicHubReputationInterface(ethicHubStorage.getAddress(keccak256("contract.name", "reputation"))); | |
require(reputation != address(0)); | |
if (delayDays > 0) { | |
ethicHubStorage.setUint(keccak256("lending.delayDays", this), delayDays); | |
reputation.burnReputation(delayDays); | |
} else { | |
uint completedProjectsByTier = ethicHubStorage.getUint(keccak256("community.completedProjectsByTier", this, tier)).add(1); | |
ethicHubStorage.setUint(keccak256("community.completedProjectsByTier", this, tier), completedProjectsByTier); | |
reputation.incrementReputation(completedProjectsByTier); | |
} | |
} | |
function getDelayDays(uint date) public view returns(uint) { | |
uint lendingDaysSeconds = lendingDays * 1 days; | |
uint defaultTime = fundingEndTime.add(lendingDaysSeconds); | |
if (date < defaultTime) { | |
return 0; | |
} else { | |
return date.sub(defaultTime).div(60).div(60).div(24); | |
} | |
} | |
// lendingInterestRate with 2 decimal | |
// 15 * (lending days)/ 365 + 4% local node fee + 3% LendingDev fee | |
function lendingInterestRatePercentage() public view returns(uint256){ | |
return annualInterest.mul(interestBaseUint).mul(lendingDays.add(getDelayDays(now))).div(365).add(localNodeFee.mul(interestBaseUint)).add(ethichubFee.mul(interestBaseUint)).add(interestBasePercent); | |
} | |
// lendingInterestRate with 2 decimal | |
function investorInterest() public view returns(uint256){ | |
return annualInterest.mul(interestBaseUint).mul(lendingDays.add(getDelayDays(now))).div(365).add(interestBasePercent); | |
} | |
function borrowerReturnFiatAmount() public view returns(uint256) { | |
return totalLendingFiatAmount.mul(lendingInterestRatePercentage()).div(interestBasePercent); | |
} | |
function borrowerReturnAmount() public view returns(uint256) { | |
return borrowerReturnFiatAmount().div(borrowerReturnEthPerFiatRate); | |
} | |
function isContribPeriodRunning() public view returns(bool) { | |
return fundingStartTime <= now && fundingEndTime > now && !capReached; | |
} | |
function checkInvestorContribution(address investor) public view returns(uint256){ | |
return investors[investor].amount; | |
} | |
function checkInvestorReturns(address investor) public view returns(uint256) { | |
uint256 investorAmount = 0; | |
if (state == LendingState.ContributionReturned) { | |
investorAmount = investors[investor].amount; | |
if (surplusEth > 0){ | |
investorAmount = investors[investor].amount.mul(totalLendingAmount).div(totalContributed); | |
} | |
return investorAmount.mul(initialEthPerFiatRate).mul(investorInterest()).div(borrowerReturnEthPerFiatRate).div(interestBasePercent); | |
} else if (state == LendingState.Default){ | |
investorAmount = investors[investor].amount; | |
// contribution = contribution * partial_funds / total_funds | |
return investorAmount.mul(returnedEth).div(totalLendingAmount); | |
} else { | |
return 0; | |
} | |
} | |
function getMaxDelayDays() public view returns(uint256){ | |
return ethicHubStorage.getUint(keccak256("lending.maxDelayDays", this)); | |
} | |
function getUserContributionReclaimStatus(address userAddress) public view returns(bool isCompensated, bool surplusEthReclaimed){ | |
isCompensated = investors[userAddress].isCompensated; | |
surplusEthReclaimed = investors[userAddress].surplusEthReclaimed; | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment