diff --git a/contracts/.solhint.json b/contracts/.solhint.json new file mode 100644 index 00000000..5b6d43e4 --- /dev/null +++ b/contracts/.solhint.json @@ -0,0 +1,12 @@ +{ + "extends": "default", + "rules": { + "not-rely-on-time": false, + "separate-by-one-line-in-contract": "warn", + "separate-by-one-line-in-contract": "warn", + "expression-indent": "warn", + "indent": "warn", + "func-order": "warn", + "statement-indent": "warn" + } +} diff --git a/contracts/LoanManager.sol b/contracts/LoanManager.sol index bb50d824..ee71b5dc 100644 --- a/contracts/LoanManager.sol +++ b/contracts/LoanManager.sol @@ -22,51 +22,50 @@ import "./MonetarySupervisor.sol"; contract LoanManager is Restricted { using SafeMath for uint256; - enum LoanState { Open, Repaid, Defaulted } + uint16 public constant CHUNK_SIZE = 100; + + enum LoanState { Open, Repaid, Defaulted, Collected } // NB: Defaulted state is not stored, only getters calculate struct LoanProduct { - uint term; // 0 - uint discountRate; // 1: discountRate in parts per million , ie. 10,000 = 1% - uint collateralRatio; // 2: loan token amount / colleteral pegged ccy value - // in parts per million , ie. 10,000 = 1% - uint minDisbursedAmount; // 3: with 4 decimals, e.g. 31000 = 3.1ACE - uint defaultingFeePt; // 4: % of collateral in parts per million , ie. 50,000 = 5% - bool isActive; // 5 + uint minDisbursedAmount; // 0: with decimals set in AugmintToken.decimals + uint32 term; // 1 + uint32 discountRate; // 2: discountRate in parts per million , ie. 10,000 = 1% + uint32 collateralRatio; // 3: loan token amount / colleteral pegged ccy value + // in parts per million , ie. 10,000 = 1% + uint32 defaultingFeePt; // 4: % of collateral in parts per million , ie. 50,000 = 5% + bool isActive; // 5 } + /* NB: we don't need to store loan parameters because loan products can't be altered (only disabled/enabled) */ struct LoanData { - address borrower; // 0 - LoanState state; // 1 - uint collateralAmount; // 2 - uint repaymentAmount; // 3 - uint loanAmount; // 4 - uint interestAmount; // 5 - uint term; // 6 - uint disbursementDate; // 7 - uint maturity; // 8 - uint defaultingFeePt; // 9 + uint collateralAmount; // 0 + uint repaymentAmount; // 1 + address borrower; // 2 + uint32 productId; // 3 + LoanState state; // 4 + uint40 maturity; // 5 } LoanProduct[] public products; LoanData[] public loans; - mapping(address => uint[]) public mLoans; // owner account address => array of loan Ids + mapping(address => uint[]) public accountLoans; // owner account address => array of loan Ids Rates public rates; // instance of ETH/pegged currency rate provider contract AugmintTokenInterface public augmintToken; // instance of token contract MonetarySupervisor public monetarySupervisor; InterestEarnedAccount public interestEarnedAccount; - event NewLoan(uint productId, uint loanId, address borrower, uint collateralAmount, uint loanAmount, - uint repaymentAmount); + event NewLoan(uint32 productId, uint loanId, address indexed borrower, uint collateralAmount, uint loanAmount, + uint repaymentAmount, uint40 maturity); - event LoanProductActiveStateChanged(uint productId, bool newState); + event LoanProductActiveStateChanged(uint32 productId, bool newState); - event LoanProductAdded(uint productId); + event LoanProductAdded(uint32 productId); event LoanRepayed(uint loanId, address borrower); - event LoanCollected(uint indexed loanId, address indexed borrower, uint collectedCollateral, + event LoanCollected(uint loanId, address indexed borrower, uint collectedCollateral, uint releasedCollateral, uint defaultingFee); function LoanManager(AugmintTokenInterface _augmintToken, MonetarySupervisor _monetarySupervisor, Rates _rates, @@ -78,50 +77,54 @@ contract LoanManager is Restricted { interestEarnedAccount = _interestEarnedAccount; } - function addLoanProduct(uint _term, uint _discountRate, uint _collateralRatio, uint _minDisbursedAmount, - uint _defaultingFee, bool _isActive) - external restrict("MonetaryBoard") returns (uint newProductId) { - newProductId = products.push( - LoanProduct(_term, _discountRate, _collateralRatio, _minDisbursedAmount, _defaultingFee, _isActive) + function addLoanProduct(uint32 term, uint32 discountRate, uint32 collateralRatio, uint minDisbursedAmount, + uint32 defaultingFeePt, bool isActive) + external restrict("MonetaryBoard") { + + uint _newProductId = products.push( + LoanProduct(minDisbursedAmount, term, discountRate, collateralRatio, defaultingFeePt, isActive) ) - 1; + uint32 newProductId = uint32(_newProductId); + require(newProductId == _newProductId); + LoanProductAdded(newProductId); - return newProductId; } - function setLoanProductActiveState(uint8 productId, bool newState) + function setLoanProductActiveState(uint32 productId, bool newState) external restrict ("MonetaryBoard") { products[productId].isActive = false; LoanProductActiveStateChanged(productId, newState); } - function newEthBackedLoan(uint8 productId) external payable { + function newEthBackedLoan(uint32 productId) external payable { LoanProduct storage product = products[productId]; require(product.isActive); // valid productId? // calculate loan values based on ETH sent in with Tx uint tokenValue = rates.convertFromWei(augmintToken.peggedSymbol(), msg.value); - uint repaymentAmount = tokenValue.mul(product.collateralRatio).roundedDiv(1000000); + uint repaymentAmount = tokenValue.mul(product.collateralRatio).div(1000000); - uint loanAmount = tokenValue.mul(product.collateralRatio) - .mul(product.discountRate) - .roundedDiv(1000000 * 1000000); + uint loanAmount; + (loanAmount, ) = calculateLoanValues(product, repaymentAmount); require(loanAmount >= product.minDisbursedAmount); - uint interestAmount = loanAmount > repaymentAmount ? 0 : repaymentAmount.sub(loanAmount); + + uint expiration = now.add(product.term); + uint40 maturity = uint40(expiration); + require(maturity == expiration); // Create new loan - uint loanId = loans.push( - LoanData(msg.sender, LoanState.Open, msg.value, repaymentAmount, loanAmount, - interestAmount, product.term, now, now + product.term, product.defaultingFeePt)) - 1; + uint loanId = loans.push(LoanData(msg.value, repaymentAmount, msg.sender, + productId, LoanState.Open, maturity)) - 1; // Store ref to new loan - mLoans[msg.sender].push(loanId); + accountLoans[msg.sender].push(loanId); // Issue tokens and send to borrower monetarySupervisor.issueLoan(msg.sender, loanAmount); - NewLoan(productId, loanId, msg.sender, msg.value, loanAmount, repaymentAmount); + NewLoan(productId, loanId, msg.sender, msg.value, loanAmount, repaymentAmount, maturity); } function collect(uint[] loanIds) external { @@ -133,32 +136,37 @@ contract LoanManager is Restricted { uint totalLoanAmountCollected; uint totalCollateralToCollect; for (uint i = 0; i < loanIds.length; i++) { - uint loanId = loanIds[i]; - require(loans[loanId].state == LoanState.Open); - require(now >= loans[loanId].maturity); + LoanData storage loan = loans[loanIds[i]]; + require(loan.state == LoanState.Open); + require(now >= loan.maturity); + LoanProduct storage product = products[loan.productId]; + + uint loanAmount; + (loanAmount, ) = calculateLoanValues(product, loan.repaymentAmount); - totalLoanAmountCollected = totalLoanAmountCollected.add(loans[loanId].loanAmount); + totalLoanAmountCollected = totalLoanAmountCollected.add(loanAmount); - loans[loanId].state = LoanState.Defaulted; + loan.state = LoanState.Collected; // send ETH collateral to augmintToken reserve - uint defaultingFeeInToken = loans[loanId].repaymentAmount.mul(loans[loanId].defaultingFeePt).div(1000000); + uint defaultingFeeInToken = loan.repaymentAmount.mul(product.defaultingFeePt).div(1000000); uint defaultingFee = rates.convertToWei(augmintToken.peggedSymbol(), defaultingFeeInToken); - uint targetCollection = rates.convertToWei(augmintToken.peggedSymbol(), loans[loanId].repaymentAmount) - .add(defaultingFee); + uint targetCollection = rates.convertToWei(augmintToken.peggedSymbol(), + loan.repaymentAmount).add(defaultingFee); + uint releasedCollateral; - if (targetCollection < loans[loanId].collateralAmount) { - releasedCollateral = loans[loanId].collateralAmount.sub(targetCollection); - loans[loanId].borrower.transfer(releasedCollateral); + if (targetCollection < loan.collateralAmount) { + releasedCollateral = loan.collateralAmount.sub(targetCollection); + loan.borrower.transfer(releasedCollateral); } - uint collateralToCollect = loans[loanId].collateralAmount.sub(releasedCollateral); + uint collateralToCollect = loan.collateralAmount.sub(releasedCollateral); if (defaultingFee > collateralToCollect) { defaultingFee = collateralToCollect; } totalCollateralToCollect = totalCollateralToCollect.add(collateralToCollect); - LoanCollected(loanId, loans[loanId].borrower, collateralToCollect, releasedCollateral, defaultingFee); + LoanCollected(loanIds[i], loan.borrower, collateralToCollect, releasedCollateral, defaultingFee); } if (totalCollateralToCollect > 0) { @@ -169,16 +177,74 @@ contract LoanManager is Restricted { } + function getProductCount() external view returns (uint ct) { + return products.length; + } + + // returns CHUNK_SIZE loan products starting from some offset: + // [ productId, minDisbursedAmount, term, discountRate, collateralRatio, defaultingFeePt, isActive ] + function getProducts(uint offset) external view returns (uint[7][CHUNK_SIZE] response) { + for (uint16 i = 0; i < CHUNK_SIZE; i++) { + + if (offset + i >= products.length) { break; } + + LoanProduct storage product = products[offset + i]; + + response[i] = [offset + i, product.minDisbursedAmount, product.term, product.discountRate, + product.collateralRatio, product.defaultingFeePt, product.isActive ? 1 : 0 ]; + } + } + function getLoanCount() external view returns (uint ct) { return loans.length; } - function getProductCount() external view returns (uint ct) { - return products.length; + /* returns CHUNK_SIZE loans starting from some offset. Loans data encoded as: + [loanId, collateralAmount, repaymentAmount, borrower, productId, state, maturity, disbursementTime, + loanAmount, interestAmount ] */ + function getLoans(uint offset) external view returns (uint[10][CHUNK_SIZE] response) { + + for (uint16 i = 0; i < CHUNK_SIZE; i++) { + + if (offset + i >= loans.length) { break; } + + response[i] = getLoanTuple(offset + i); + } } - function getLoanIds(address borrower) external view returns (uint[] _loans) { - return mLoans[borrower]; + function getLoanCountForAddress(address borrower) external view returns (uint) { + return accountLoans[borrower].length; + } + + /* returns CHUNK_SIZE loans of a given account, starting from some offset. Loans data encoded as: + [loanId, collateralAmount, repaymentAmount, borrower, productId, state, maturity, disbursementTime, + loanAmount, interestAmount ] */ + function getLoansForAddress(address borrower, uint offset) external view returns (uint[10][CHUNK_SIZE] response) { + + uint[] storage loansForAddress = accountLoans[borrower]; + + for (uint16 i = 0; i < CHUNK_SIZE; i++) { + + if (offset + i >= loansForAddress.length) { break; } + + response[i] = getLoanTuple(loansForAddress[offset + i]); + } + } + + function getLoanTuple(uint loanId) public view returns (uint[10] result) { + LoanData storage loan = loans[loanId]; + LoanProduct storage product = products[loan.productId]; + + uint loanAmount; + uint interestAmount; + (loanAmount, interestAmount) = calculateLoanValues(product, loan.repaymentAmount); + uint disbursementTime = loan.maturity - product.term; + + LoanState loanState = + loan.state == LoanState.Open && now >= loan.maturity ? LoanState.Defaulted : loan.state; + + result = [loanId, loan.collateralAmount, loan.repaymentAmount, uint(loan.borrower), + loan.productId, uint(loanState), loan.maturity, disbursementTime, loanAmount, interestAmount]; } /* repay loan, called from AugmintToken's transferAndNotify @@ -193,21 +259,40 @@ contract LoanManager is Restricted { _repayLoan(loanId, repaymentAmount); } + function calculateLoanValues(LoanProduct storage product, uint repaymentAmount) + internal view returns (uint loanAmount, uint interestAmount) { + // calculate loan values based on repayment amount + loanAmount = repaymentAmount.mul(product.discountRate).div(1000000); + interestAmount = loanAmount > repaymentAmount ? 0 : repaymentAmount.sub(loanAmount); + } + /* internal function, assuming repayment amount already transfered */ function _repayLoan(uint loanId, uint repaymentAmount) internal { - require(loans[loanId].state == LoanState.Open); - require(now <= loans[loanId].maturity); - require(loans[loanId].repaymentAmount == repaymentAmount); + LoanData storage loan = loans[loanId]; + require(loan.state == LoanState.Open); + require(repaymentAmount == loan.repaymentAmount); + require(now <= loan.maturity); + + LoanProduct storage product = products[loan.productId]; + uint loanAmount; + uint interestAmount; + (loanAmount, interestAmount) = calculateLoanValues(product, loan.repaymentAmount); + loans[loanId].state = LoanState.Repaid; - augmintToken.transfer(interestEarnedAccount, loans[loanId].interestAmount); + if (interestAmount > 0) { + augmintToken.transfer(interestEarnedAccount, interestAmount); + augmintToken.burn(loanAmount); + } else { + // negative or zero interest (i.e. discountRate >= 0) + augmintToken.burn(repaymentAmount); + } - augmintToken.burn(loans[loanId].loanAmount); - monetarySupervisor.loanRepaymentNotification(loans[loanId].loanAmount); // update KPIs + monetarySupervisor.loanRepaymentNotification(loanAmount); // update KPIs - loans[loanId].borrower.transfer(loans[loanId].collateralAmount); // send back ETH collateral + loan.borrower.transfer(loan.collateralAmount); // send back ETH collateral - LoanRepayed(loanId, loans[loanId].borrower); + LoanRepayed(loanId, loan.borrower); } } diff --git a/test/helpers/loanTestHelpers.js b/test/helpers/loanTestHelpers.js index 77fe1ca6..5955fd87 100644 --- a/test/helpers/loanTestHelpers.js +++ b/test/helpers/loanTestHelpers.js @@ -7,10 +7,12 @@ const Rates = artifacts.require("./Rates.sol"); const tokenTestHelpers = require("./tokenTestHelpers.js"); const testHelpers = require("./testHelpers.js"); -const NEWLOAN_MAX_GAS = 350000; -const REPAY_MAX_GAS = 150000; +const NEWLOAN_MAX_GAS = 220000; +const REPAY_MAX_GAS = 120000; const COLLECT_BASE_GAS = 100000; +let CHUNK_SIZE = null; + let augmintToken = null; let monetarySupervisor = null; let loanManager = null; @@ -23,16 +25,21 @@ module.exports = { createLoan, repayLoan, collectLoan, - getProductInfo, + getProductsInfo, + parseLoansInfo, calcLoanValues, loanAsserts, get loanManager() { return loanManager; + }, + get CHUNK_SIZE() { + return CHUNK_SIZE; } }; before(async function() { loanManager = LoanManager.at(LoanManager.address); + CHUNK_SIZE = (await loanManager.CHUNK_SIZE()).toNumber(); augmintToken = tokenTestHelpers.augmintToken; monetarySupervisor = tokenTestHelpers.monetarySupervisor; @@ -61,18 +68,19 @@ async function createLoan(testInstance, product, borrower, collateralWei) { const tx = await loanManager.newEthBackedLoan(loan.product.id, { from: loan.borrower, - value: loan.collateral + value: loan.collateralAmount }); testHelpers.logGasUse(testInstance, tx, "newEthBackedLoan"); const [newLoanEvenResult, ,] = await Promise.all([ testHelpers.assertEvent(loanManager, "NewLoan", { loanId: x => x, - productId: loan.product.id, + productId: loan.product.id.toNumber(), borrower: loan.borrower, - collateralAmount: loan.collateral.toString(), + collateralAmount: loan.collateralAmount.toString(), loanAmount: loan.loanAmount.toString(), - repaymentAmount: loan.repaymentAmount.toString() + repaymentAmount: loan.repaymentAmount.toString(), + maturity: x => x }), testHelpers.assertEvent(augmintToken, "AugmintTransfer", { @@ -92,6 +100,7 @@ async function createLoan(testInstance, product, borrower, collateralWei) { ]); loan.id = newLoanEvenResult.loanId.toNumber(); + loan.maturity = newLoanEvenResult.maturity.toNumber(); const [totalSupplyAfter, totalLoanAmountAfter, ,] = await Promise.all([ augmintToken.totalSupply(), @@ -103,11 +112,11 @@ async function createLoan(testInstance, product, borrower, collateralWei) { reserve: {}, borrower: { ace: balBefore.borrower.ace.add(loan.loanAmount), - eth: balBefore.borrower.eth.minus(loan.collateral), + eth: balBefore.borrower.eth.minus(loan.collateralAmount), gasFee: NEWLOAN_MAX_GAS * testHelpers.GAS_PRICE }, loanManager: { - eth: balBefore.loanManager.eth.plus(loan.collateral) + eth: balBefore.loanManager.eth.plus(loan.collateralAmount) }, interestEarned: {} }) @@ -173,11 +182,11 @@ async function repayLoan(testInstance, loan) { reserve: {}, borrower: { ace: balBefore.borrower.ace.sub(loan.repaymentAmount), - eth: balBefore.borrower.eth.add(loan.collateral), + eth: balBefore.borrower.eth.add(loan.collateralAmount), gasFee: REPAY_MAX_GAS * testHelpers.GAS_PRICE }, loanManager: { - eth: balBefore.loanManager.eth.minus(loan.collateral) + eth: balBefore.loanManager.eth.minus(loan.collateralAmount) }, interestEarned: { ace: balBefore.interestEarned.ace.add(loan.interestAmount) @@ -187,8 +196,11 @@ async function repayLoan(testInstance, loan) { assert.equal( totalSupplyAfter.toString(), - totalSupplyBefore.sub(loan.loanAmount).toString(), - "total ACE supply should be reduced by the loan amount (what was disbursed)" + totalSupplyBefore + .sub(loan.repaymentAmount) + .add(loan.interestAmount) + .toString(), + "total supply should be reduced by the repayment amount less interestAmount" ); assert.equal( totalLoanAmountAfter.toString(), @@ -199,7 +211,7 @@ async function repayLoan(testInstance, loan) { async function collectLoan(testInstance, loan, collector) { loan.collector = collector; - loan.state = 2; // defaulted + loan.state = 3; // Collected const targetCollectionInToken = loan.repaymentAmount.mul(loan.product.defaultingFeePt.add(1000000)).div(1000000); const targetFeeInToken = loan.repaymentAmount.mul(loan.product.defaultingFeePt).div(1000000); @@ -224,14 +236,14 @@ async function collectLoan(testInstance, loan, collector) { loanManager: loanManager.address, interestEarned: interestEarnedAcc }), - rates.convertFromWei(peggedSymbol, loan.collateral), + rates.convertFromWei(peggedSymbol, loan.collateralAmount), rates.convertToWei(peggedSymbol, loan.repaymentAmount), rates.convertToWei(peggedSymbol, targetCollectionInToken), rates.convertToWei(peggedSymbol, targetFeeInToken) ]); - const releasedCollateral = BigNumber.max(loan.collateral.sub(targetCollectionInWei), 0); - const collectedCollateral = loan.collateral.sub(releasedCollateral); + const releasedCollateral = BigNumber.max(loan.collateralAmount.sub(targetCollectionInWei), 0); + const collectedCollateral = loan.collateralAmount.sub(releasedCollateral); const defaultingFee = BigNumber.min(targetFeeInWei, collectedCollateral); // const rate = await rates.rates("EUR"); @@ -240,7 +252,7 @@ async function collectLoan(testInstance, loan, collector) { // A-EUR/EUR: ${rate[0] / 10000} // defaulting fee pt: ${loan.product.defaultingFeePt / 10000} % // repaymentAmount: ${loan.repaymentAmount / 10000} A-EUR = ${web3.fromWei(repaymentAmountInWei)} ETH - // collateral: ${web3.fromWei(loan.collateral).toString()} ETH = ${collateralInToken / 10000} A-EUR + // collateral: ${web3.fromWei(loan.collateralAmount).toString()} ETH = ${collateralInToken / 10000} A-EUR // -------------------- // targetFee: ${targetFeeInToken / 10000} A-EUR = ${web3.fromWei(targetFeeInWei).toString()} ETH // target collection : ${targetCollectionInToken / 10000} A-EUR = ${web3 @@ -251,16 +263,8 @@ async function collectLoan(testInstance, loan, collector) { // defaultingFee: ${web3.fromWei(defaultingFee).toString()} ETH` // ); - // console.log( - // "DEBUG. Borrower balance before collection:", - // ((await web3.eth.getBalance(loan.borrower)) / ONE_ETH).toString() - // ); const tx = await loanManager.collect([loan.id], { from: loan.collector }); testHelpers.logGasUse(testInstance, tx, "collect 1"); - // console.log( - // "DEBUG. Borrower balance after collection:", - // ((await web3.eth.getBalance(loan.borrower)) / ONE_ETH).toString() - // ); const [totalSupplyAfter, totalLoanAmountAfter, , ,] = await Promise.all([ augmintToken.totalSupply(), @@ -290,7 +294,7 @@ async function collectLoan(testInstance, loan, collector) { }, loanManager: { - eth: balBefore.loanManager.eth.minus(loan.collateral) + eth: balBefore.loanManager.eth.minus(loan.collateralAmount) }, interestEarned: {} @@ -305,39 +309,78 @@ async function collectLoan(testInstance, loan, collector) { ); } -async function getProductInfo(productId) { - const prod = await loanManager.products(productId); - const info = { - id: productId, - term: prod[0], - discountRate: prod[1], - collateralRatio: prod[2], - minDisbursedAmount: prod[3], - defaultingFeePt: prod[4], - isActive: prod[5] - }; - return info; +async function getProductsInfo(offset) { + const products = await loanManager.getProducts(offset); + assert.equal(products.length, CHUNK_SIZE); + const result = []; + products.map(prod => { + const [id, minDisbursedAmount, term, discountRate, collateralRatio, defaultingFeePt, isActive] = prod; + if (term.gt(0)) { + result.push({ id, minDisbursedAmount, term, discountRate, collateralRatio, defaultingFeePt, isActive }); + } + }); + return result; +} + +/* parse array returned by getLoans & getLoansForAddress */ +function parseLoansInfo(loans) { + assert.equal(loans.length, CHUNK_SIZE); + const result = []; + loans.map(loan => { + const [ + id, + collateralAmount, + repaymentAmount, + borrower, + productId, + state, + maturity, + disbursementTime, + loanAmount, + interestAmount + ] = loan; + + if (maturity.gt(0)) { + result.push({ + id, + collateralAmount, + repaymentAmount, + borrower, + productId, + state, + maturity, + disbursementTime, + loanAmount, + interestAmount + }); + } + }); + + return result; } async function calcLoanValues(rates, product, collateralWei) { const ret = {}; const ppmDiv = 1000000; - ret.collateral = new BigNumber(collateralWei); + ret.collateralAmount = new BigNumber(collateralWei); ret.tokenValue = await rates.convertFromWei(peggedSymbol, collateralWei); ret.repaymentAmount = ret.tokenValue .mul(product.collateralRatio) .div(ppmDiv) - .round(0, BigNumber.ROUND_HALF_UP); + .round(0, BigNumber.ROUND_DOWN); ret.loanAmount = ret.tokenValue .mul(product.collateralRatio) .mul(product.discountRate) .div(ppmDiv * ppmDiv) - .round(0, BigNumber.ROUND_HALF_UP); + .round(0, BigNumber.ROUND_DOWN); + + ret.interestAmount = ret.repaymentAmount.gt(ret.loanAmount) + ? ret.repaymentAmount.minus(ret.loanAmount) + : new BigNumber(0); - ret.interestAmount = ret.repaymentAmount.minus(ret.loanAmount); ret.disbursementTime = moment() .utc() .unix(); @@ -348,30 +391,20 @@ async function calcLoanValues(rates, product, collateralWei) { async function loanAsserts(expLoan) { const loan = await loanManager.loans(expLoan.id); - assert.equal(loan[0], expLoan.borrower, "borrower should be set"); - assert.equal(loan[1].toNumber(), expLoan.state, "loan state should be set"); - assert.equal(loan[2].toString(), expLoan.collateral.toString(), "collateralAmount should be set"); - assert.equal(loan[3].toString(), expLoan.repaymentAmount.toString(), "repaymentAmount should be set"); - assert.equal(loan[4].toString(), expLoan.loanAmount.toString(), "loanAmount should be set"); - assert.equal(loan[5].toString(), expLoan.interestAmount.toString(), "interestAmount should be set"); - assert.equal(loan[6].toString(), expLoan.product.term.toString(), "term should be set"); - - const disbursementTimeActual = loan[7]; - assert( - disbursementTimeActual >= expLoan.disbursementTime, - "disbursementDate should be at least the time at disbursement" - ); - assert( - disbursementTimeActual <= expLoan.disbursementTime + 5, - "disbursementDate should be at most the time at disbursement + 5. Difference is: " + - (disbursementTimeActual - expLoan.disbursementTime) - ); + assert.equal(loan[0].toString(), expLoan.collateralAmount.toString(), "collateralAmount should be set"); + assert.equal(loan[1].toString(), expLoan.repaymentAmount.toString(), "repaymentAmount should be set"); + assert.equal(loan[2], expLoan.borrower, "borrower should be set"); + assert.equal(loan[3].toNumber(), expLoan.product.id, "product id should be set"); + assert.equal(loan[4].toNumber(), expLoan.state, "loan state should be set"); + assert.equal(loan[5].toNumber(), expLoan.maturity, "maturity should be the same as in NewLoan event"); - assert.equal( - loan[8].toString(), - disbursementTimeActual.add(expLoan.product.term), - "maturity should be at disbursementDate + term" - ); + const maturityActual = loan[5]; + const maturityExpected = expLoan.product.term.add(expLoan.disbursementTime).toNumber(); - assert.equal(loan[9].toString(), expLoan.product.defaultingFeePt.toString(), "defaultingFeePt should be set"); + assert(maturityActual >= maturityExpected, "maturity should be at least term + the time at disbursement"); + assert( + maturityActual <= maturityExpected + 5, + "maturity should be at most the term + time at disbursement + 5. Difference is: " + + (maturityActual - maturityExpected) + ); } diff --git a/test/loanCollection.js b/test/loanCollection.js new file mode 100644 index 00000000..56a2ca8b --- /dev/null +++ b/test/loanCollection.js @@ -0,0 +1,151 @@ +const testHelpers = require("./helpers/testHelpers.js"); +const tokenTestHelpers = require("./helpers/tokenTestHelpers.js"); +const loanTestHelpers = require("./helpers/loanTestHelpers.js"); +const ratesTestHelpers = require("./helpers/ratesTestHelpers.js"); + +let loanManager = null; +let monetarySupervisor = null; +let rates = null; +let products = {}; + +contract("Loans tests", accounts => { + before(async function() { + rates = ratesTestHelpers.rates; + monetarySupervisor = tokenTestHelpers.monetarySupervisor; + loanManager = loanTestHelpers.loanManager; + await tokenTestHelpers.issueToReserve(1000000000); + + const prodCount = (await loanManager.getProductCount()).toNumber(); + // These neeed to be sequantial b/c product order assumed when retreving via getProducts + // term (in sec), discountRate, loanCoverageRatio, minDisbursedAmount (w/ 2 decimals), defaultingFeePt, isActive + await loanManager.addLoanProduct(86400, 970000, 850000, 3000, 50000, true); // notDue + await loanManager.addLoanProduct(1, 970000, 850000, 1000, 50000, true); // defaulting + await loanManager.addLoanProduct(1, 900000, 900000, 1000, 100000, true); // defaultingNoLeftOver + await loanManager.addLoanProduct(1, 1000000, 900000, 2000, 50000, true); // zeroInterest + await loanManager.addLoanProduct(1, 1100000, 900000, 2000, 50000, true); // negativeInterest + await loanManager.addLoanProduct(1, 990000, 1000000, 2000, 50000, true); // fullCoverage + await loanManager.addLoanProduct(1, 990000, 1200000, 2000, 50000, true); // moreCoverage + + const [newProducts] = await Promise.all([ + loanTestHelpers.getProductsInfo(prodCount), + tokenTestHelpers.withdrawFromReserve(accounts[0], 1000000000) + ]); + [ + products.notDue, + products.defaulting, + products.defaultingNoLeftOver, + products.zeroInterest, + products.negativeInterest, + products.fullCoverage, + products.moreCoverage + ] = newProducts; + }); + + it("Should collect a defaulted A-EUR loan and send back leftover collateral ", async function() { + const loan = await loanTestHelpers.createLoan(this, products.defaulting, accounts[1], web3.toWei(0.5)); + + await testHelpers.waitForTimeStamp(loan.maturity); + + await loanTestHelpers.collectLoan(this, loan, accounts[2]); + }); + + it("Should collect a defaulted A-EUR loan when no leftover collateral (collection exactly covered)", async function() { + await rates.setRate("EUR", 100000); + const loan = await loanTestHelpers.createLoan(this, products.defaultingNoLeftOver, accounts[1], web3.toWei(1)); + + await Promise.all([rates.setRate("EUR", 99000), testHelpers.waitForTimeStamp(loan.maturity)]); + + await loanTestHelpers.collectLoan(this, loan, accounts[2]); + }); + + it("Should collect a defaulted A-EUR loan when no leftover collateral (collection partially covered)", async function() { + await rates.setRate("EUR", 100000); + const loan = await loanTestHelpers.createLoan(this, products.defaultingNoLeftOver, accounts[1], web3.toWei(2)); + + await Promise.all([rates.setRate("EUR", 98900), testHelpers.waitForTimeStamp(loan.maturity)]); + + await loanTestHelpers.collectLoan(this, loan, accounts[2]); + }); + + it("Should collect a defaulted A-EUR loan when no leftover collateral (only fee covered)", async function() { + await rates.setRate("EUR", 99800); + const loan = await loanTestHelpers.createLoan(this, products.defaultingNoLeftOver, accounts[1], web3.toWei(2)); + await Promise.all([rates.setRate("EUR", 1), testHelpers.waitForTimeStamp(loan.maturity)]); + + await loanTestHelpers.collectLoan(this, loan, accounts[2]); + await rates.setRate("EUR", 99800); // restore rates + }); + + it("Should get and collect a loan with discountRate = 1 (zero interest)", async function() { + const loan = await loanTestHelpers.createLoan(this, products.zeroInterest, accounts[0], web3.toWei(0.5)); + await loanTestHelpers.collectLoan(this, loan, accounts[2]); + }); + + it("Should get and collect a loan with discountRate > 1 (negative interest)", async function() { + const loan = await loanTestHelpers.createLoan(this, products.negativeInterest, accounts[0], web3.toWei(0.5)); + await loanTestHelpers.collectLoan(this, loan, accounts[2]); + }); + + it("Should get and collect a loan with collateralRatio = 1", async function() { + const loan = await loanTestHelpers.createLoan(this, products.fullCoverage, accounts[0], web3.toWei(0.5)); + await loanTestHelpers.collectLoan(this, loan, accounts[2]); + }); + + it("Should get and collect a loan with collateralRatio > 1", async function() { + const loan = await loanTestHelpers.createLoan(this, products.moreCoverage, accounts[0], web3.toWei(0.5)); + await loanTestHelpers.collectLoan(this, loan, accounts[2]); + }); + + it("Should collect multiple defaulted loans", async function() { + const loanCount = (await loanManager.getLoanCount()).toNumber(); + await Promise.all([ + loanManager.newEthBackedLoan(products.zeroInterest.id, { from: accounts[0], value: web3.toWei(0.2) }), + loanManager.newEthBackedLoan(products.fullCoverage.id, { from: accounts[1], value: web3.toWei(0.2) }), + loanManager.newEthBackedLoan(products.negativeInterest.id, { from: accounts[1], value: web3.toWei(0.2) }) + ]); + + await testHelpers.waitFor(1000); + + const tx = await loanManager.collect([loanCount, loanCount + 1, loanCount + 2]); + testHelpers.logGasUse(this, tx, "collect 3"); + }); + + it("Should NOT collect multiple loans if one is not due", async function() { + const loanCount = (await loanManager.getLoanCount()).toNumber(); + await Promise.all([ + loanManager.newEthBackedLoan(products.notDue.id, { from: accounts[0], value: web3.toWei(0.2) }), + loanManager.newEthBackedLoan(products.defaulting.id, { from: accounts[1], value: web3.toWei(0.2) }) + ]); + + await testHelpers.waitFor(1000); + + await testHelpers.expectThrow(loanManager.collect([loanCount, loanCount + 1])); + }); + + it("Should NOT collect a loan before it's due", async function() { + const loan = await loanTestHelpers.createLoan(this, products.notDue, accounts[1], web3.toWei(0.5)); + await testHelpers.expectThrow(loanManager.collect([loan.id])); + }); + + it("Should not collect when rate = 0", async function() { + await rates.setRate("EUR", 99800); + const loan = await loanTestHelpers.createLoan(this, products.defaultingNoLeftOver, accounts[1], web3.toWei(2)); + await Promise.all([rates.setRate("EUR", 0), testHelpers.waitForTimeStamp(loan.maturity)]); + + testHelpers.expectThrow(loanTestHelpers.collectLoan(this, loan, accounts[2])); + await rates.setRate("EUR", 99800); + }); + + it("Should NOT collect an already collected loan", async function() { + const loan = await loanTestHelpers.createLoan(this, products.defaulting, accounts[1], web3.toWei(0.5)); + + await testHelpers.waitForTimeStamp(loan.maturity); + + await loanTestHelpers.collectLoan(this, loan, accounts[2]); + await testHelpers.expectThrow(loanManager.collect([loan.id])); + }); + + it("only allowed contract should call MonetarySupervisor.loanCollectionNotification", async function() { + await testHelpers.expectThrow(monetarySupervisor.loanCollectionNotification(0, { from: accounts[0] })); + }); +}); diff --git a/test/loanManager.js b/test/loanManager.js index 6ffe14a8..918a265b 100644 --- a/test/loanManager.js +++ b/test/loanManager.js @@ -8,16 +8,17 @@ contract("loanManager tests", accounts => { loanManager = loanTestHelpers.loanManager; }); - it("Should add new product", async function() { + it("Should add new product allow listing from offset 0", async function() { const prod = { + // assuming prod attributes are same order as array returned + minDisbursedAmount: 300000, term: 86400, discountRate: 970000, collateralRatio: 850000, - minDisbursedAmount: 300000, defaultingFeePt: 50000, isActive: true }; - await loanManager.addLoanProduct( + const tx = await loanManager.addLoanProduct( prod.term, prod.discountRate, prod.collateralRatio, @@ -26,17 +27,70 @@ contract("loanManager tests", accounts => { prod.isActive, { from: accounts[0] } ); + testHelpers.logGasUse(this, tx, "addLoanProduct"); + const res = await testHelpers.assertEvent(loanManager, "LoanProductAdded", { productId: x => x }); - const prodActual = await loanTestHelpers.getProductInfo(res.productId); - Object.keys(prod).forEach(argName => + const prodActual = await loanManager.products(res.productId); + + Object.keys(prod).forEach((argName, index) => assert.equal( - prodActual[argName].toString(), + prodActual[index].toString(), prod[argName].toString(), - `Prod arg ${argName} expected ${prod[argName]} but has value ${prodActual[argName]}` + `Prod arg ${argName} expected ${prod[argName]} but has value ${prodActual[index]}` ) ); + + prod.id = res.productId; + const productsInfo = await loanTestHelpers.getProductsInfo(0); + const productCount = (await loanManager.getProductCount()).toNumber(); + assert.equal(productsInfo.length, productCount); + const lastProduct = productsInfo[productCount - 1]; + assert.equal(lastProduct.id.toNumber(), prod.id); + assert.equal(lastProduct.term.toNumber(), prod.term); + assert.equal(lastProduct.discountRate.toNumber(), prod.discountRate); + assert.equal(lastProduct.collateralRatio.toNumber(), prod.collateralRatio); + assert.equal(lastProduct.minDisbursedAmount.toNumber(), prod.minDisbursedAmount); + assert.equal(lastProduct.defaultingFeePt.toNumber(), prod.defaultingFeePt); + assert.equal(lastProduct.isActive.toNumber(), prod.isActive ? 1 : 0); + }); + + it("Should allow listing products (offset > 0)", async function() { + const prod = { + // assuming prod attributes are same order as array returned + minDisbursedAmount: 300000, + term: 86400, + discountRate: 970000, + collateralRatio: 850000, + defaultingFeePt: 50000, + isActive: true + }; + const tx = await loanManager.addLoanProduct( + prod.term, + prod.discountRate, + prod.collateralRatio, + prod.minDisbursedAmount, + prod.defaultingFeePt, + prod.isActive, + { from: accounts[0] } + ); + testHelpers.logGasUse(this, tx, "addLoanProduct"); + + const productCount = (await loanManager.getProductCount()).toNumber(); + prod.id = productCount - 1; + + const productsInfo = await loanTestHelpers.getProductsInfo(productCount - 1); + assert.equal(productsInfo.length, 1); + + const lastProduct = productsInfo[0]; + assert.equal(lastProduct.id.toNumber(), prod.id); + assert.equal(lastProduct.term.toNumber(), prod.term); + assert.equal(lastProduct.discountRate.toNumber(), prod.discountRate); + assert.equal(lastProduct.collateralRatio.toNumber(), prod.collateralRatio); + assert.equal(lastProduct.minDisbursedAmount.toNumber(), prod.minDisbursedAmount); + assert.equal(lastProduct.defaultingFeePt.toNumber(), prod.defaultingFeePt); + assert.equal(lastProduct.isActive.toNumber(), prod.isActive ? 1 : 0); }); it("Only allowed should add new product", async function() { diff --git a/test/loans.js b/test/loans.js index 75061c5b..7a0187b2 100644 --- a/test/loans.js +++ b/test/loans.js @@ -1,3 +1,4 @@ +const BigNumber = require("bignumber.js"); const LoanManager = artifacts.require("./LoanManager.sol"); const testHelpers = require("./helpers/testHelpers.js"); @@ -11,7 +12,7 @@ let monetarySupervisor = null; let rates = null; let products = {}; -contract("Augmint Loans tests", accounts => { +contract("Loans tests", accounts => { before(async function() { rates = ratesTestHelpers.rates; monetarySupervisor = tokenTestHelpers.monetarySupervisor; @@ -19,44 +20,52 @@ contract("Augmint Loans tests", accounts => { loanManager = loanTestHelpers.loanManager; await tokenTestHelpers.issueToReserve(1000000000); - // These neeed to be sequantial b/c ids hardcoded in tests. - // term (in sec), discountRate, loanCoverageRatio, minDisbursedAmount (w/ 2 decimals), defaultingFeePt, isActive - // notDue: (due in 1 day) const prodCount = (await loanManager.getProductCount()).toNumber(); - - await loanManager.addLoanProduct(86400, 970000, 850000, 3000, 50000, true); - // repaying: due in 60 sec for testing repayment - await loanManager.addLoanProduct(60, 985000, 900000, 2000, 50000, true); - // defaulting: due in 1 sec, repay in 1sec for testing defaults - //await loanManager.addLoanProduct(1, 990000, 600000, 100000, 50000, true); - await loanManager.addLoanProduct(1, 970000, 850000, 1000, 50000, true); - // defaulting no left over collateral: due in 1 sec, repay in 1sec for testing defaults without leftover - await loanManager.addLoanProduct(1, 900000, 900000, 1000, 100000, true); - // disabled product - await loanManager.addLoanProduct(1, 990000, 990000, 1000, 50000, false); - - [ - products.disabledProduct, - products.defaultingNoLeftOver, - products.defaulting, - products.repaying, - products.notDue, - , - ] = await Promise.all([ - loanTestHelpers.getProductInfo(prodCount + 4), - loanTestHelpers.getProductInfo(prodCount + 3), - loanTestHelpers.getProductInfo(prodCount + 2), - loanTestHelpers.getProductInfo(prodCount + 1), - loanTestHelpers.getProductInfo(prodCount), + // These neeed to be sequantial b/c product order assumed when retreving via getProducts + // term (in sec), discountRate, loanCoverageRatio, minDisbursedAmount (w/ 2 decimals), defaultingFeePt, isActive + await loanManager.addLoanProduct(86400, 970000, 850000, 3000, 50000, true); // notDue + await loanManager.addLoanProduct(60, 985000, 900000, 2000, 50000, true); // repaying + await loanManager.addLoanProduct(1, 970000, 850000, 1000, 50000, true); // defaulting + await loanManager.addLoanProduct(1, 990000, 990000, 1000, 50000, false); // disabledProduct + await loanManager.addLoanProduct(60, 1000000, 900000, 2000, 50000, true); // zeroInterest + await loanManager.addLoanProduct(60, 1100000, 900000, 2000, 50000, true); // negativeInterest + await loanManager.addLoanProduct(60, 990000, 1000000, 2000, 50000, true); // fullCoverage + await loanManager.addLoanProduct(60, 990000, 1200000, 2000, 50000, true); // moreCoverage + + const [newProducts] = await Promise.all([ + loanTestHelpers.getProductsInfo(prodCount), tokenTestHelpers.withdrawFromReserve(accounts[0], 1000000000) ]); + [ + products.notDue, + products.repaying, + products.defaulting, + products.disabledProduct, + products.zeroInterest, + products.negativeInterest, + products.fullCoverage, + products.moreCoverage + ] = newProducts; }); - it("Should get an ACE loan", async function() { + it("Should get an A-EUR loan", async function() { await loanTestHelpers.createLoan(this, products.repaying, accounts[0], web3.toWei(0.5)); }); - it("Should NOT get a loan less than minLoanAmount"); + it("Should NOT get a loan less than minDisbursedAmount", async function() { + const prod = products.repaying; + const loanAmount = prod.minDisbursedAmount + .sub(1) + .div(prod.discountRate) + .mul(1000000) + .round(0, BigNumber.ROUND_UP); + const weiAmount = (await rates.convertToWei(tokenTestHelpers.peggedSymbol, loanAmount)) + .div(prod.collateralRatio) + .mul(1000000) + .round(0, BigNumber.ROUND_UP); + + await testHelpers.expectThrow(loanManager.newEthBackedLoan(prod.id, { from: accounts[0], value: weiAmount })); + }); it("Shouldn't get a loan for a disabled product", async function() { await testHelpers.expectThrow( @@ -64,91 +73,179 @@ contract("Augmint Loans tests", accounts => { ); }); - it("Should NOT collect a loan before it's due"); - it("Should NOT repay an ACE loan on maturity if ACE balance is insufficient"); - it("should not repay a loan with smaller amount than repaymentAmount"); - it("Non owner should be able to repay a loan too"); - it("Should not repay with invalid loanId"); - - it("Should repay an ACE loan before maturity", async function() { + it("Should repay an A-EUR loan before maturity", async function() { const loan = await loanTestHelpers.createLoan(this, products.notDue, accounts[1], web3.toWei(0.5)); - // send interest to borrower to have enough ACE to repay in test + // send interest to borrower to have enough A-EUR to repay in test await augmintToken.transfer(loan.borrower, loan.interestAmount, { from: accounts[0] }); - await loanTestHelpers.repayLoan(this, loan, true); // repaymant via AugmintToken.repayLoan convenience func + await loanTestHelpers.repayLoan(this, loan); }); - it("Should collect a defaulted ACE loan and send back leftover collateral ", async function() { - const loan = await loanTestHelpers.createLoan(this, products.defaulting, accounts[1], web3.toWei(0.5)); + it("Should get and repay a loan whith discountRate = 1 (zero interest)", async function() { + const loan = await loanTestHelpers.createLoan(this, products.zeroInterest, accounts[0], web3.toWei(0.5)); + await loanTestHelpers.repayLoan(this, loan); + }); - await testHelpers.waitForTimeStamp((await loanManager.loans(loan.id))[8].toNumber()); + it("Should get and repay a loan whith discountRate > 1 (negative interest)", async function() { + const loan = await loanTestHelpers.createLoan(this, products.negativeInterest, accounts[0], web3.toWei(0.5)); + await loanTestHelpers.repayLoan(this, loan); + }); - await loanTestHelpers.collectLoan(this, loan, accounts[2]); + it("Should get and repay a loan with colletaralRatio = 1", async function() { + const loan = await loanTestHelpers.createLoan(this, products.fullCoverage, accounts[0], web3.toWei(0.5)); + await loanTestHelpers.repayLoan(this, loan); }); - it("Should collect a defaulted ACE loan when no leftover collateral (collection exactly covered)", async function() { - await rates.setRate("EUR", 100000); - const loan = await loanTestHelpers.createLoan(this, products.defaultingNoLeftOver, accounts[1], web3.toWei(1)); + it("Should get and repay a loan with colletaralRatio > 1", async function() { + const loan = await loanTestHelpers.createLoan(this, products.moreCoverage, accounts[0], web3.toWei(0.5)); + await loanTestHelpers.repayLoan(this, loan); + }); - await Promise.all([ - rates.setRate("EUR", 99000), - testHelpers.waitForTimeStamp((await loanManager.loans(loan.id))[8].toNumber()) - ]); + it("Non owner should be able to repay a loan too", async function() { + const loan = await loanTestHelpers.createLoan(this, products.notDue, accounts[1], web3.toWei(0.5)); - await loanTestHelpers.collectLoan(this, loan, accounts[2]); + await augmintToken.transferAndNotify(loanManager.address, loan.repaymentAmount, loan.id, { + from: accounts[0] + }); + + await testHelpers.assertEvent(loanManager, "LoanRepayed", { + loanId: loan.id, + borrower: loan.borrower + }); }); - it("Should collect a defaulted ACE loan when no leftover collateral (collection partially covered)", async function() { - await rates.setRate("EUR", 100000); - const loan = await loanTestHelpers.createLoan(this, products.defaultingNoLeftOver, accounts[1], web3.toWei(1)); + it("Should NOT repay an A-EUR loan on maturity if A-EUR balance is insufficient", async function() { + const borrower = accounts[2]; + const loan = await loanTestHelpers.createLoan(this, products.notDue, borrower, web3.toWei(0.5)); + const accBal = await augmintToken.balanceOf(borrower); - await Promise.all([ - rates.setRate("EUR", 98900), - testHelpers.waitForTimeStamp((await loanManager.loans(loan.id))[8].toNumber()) - ]); + // send just 0.01 A€ less than required for repayment + const topUp = loan.repaymentAmount.sub(accBal).sub(1); + assert(accBal.add(topUp).lt(loan.repaymentAmount)); // sanitiy against previous tests accidently leaving A€ borrower account + await augmintToken.transfer(borrower, topUp, { + from: accounts[0] + }); - await loanTestHelpers.collectLoan(this, loan, accounts[2]); + await testHelpers.expectThrow( + augmintToken.transferAndNotify(loanManager.address, loan.repaymentAmount, loan.id, { from: borrower }) + ); }); - it("Should collect a defaulted ACE loan when no leftover collateral (only fee covered)", async function() { - await rates.setRate("EUR", 99800); - const loan = await loanTestHelpers.createLoan(this, products.defaultingNoLeftOver, accounts[1], web3.toWei(2)); - await Promise.all([ - rates.setRate("EUR", 1), - testHelpers.waitForTimeStamp((await loanManager.loans(loan.id))[8].toNumber()) - ]); + it("should not repay a loan with smaller amount than repaymentAmount", async function() { + const borrower = accounts[1]; + const loan = await loanTestHelpers.createLoan(this, products.notDue, borrower, web3.toWei(0.2)); + + await augmintToken.transfer(loan.borrower, loan.interestAmount, { + from: accounts[0] + }); - await loanTestHelpers.collectLoan(this, loan, accounts[2]); + await testHelpers.expectThrow( + augmintToken.transferAndNotify(loanManager.address, loan.repaymentAmount.sub(1), loan.id, { + from: borrower + }) + ); }); - it("Should not collect when rate = 0", async function() { - await rates.setRate("EUR", 99800); - const loan = await loanTestHelpers.createLoan(this, products.defaultingNoLeftOver, accounts[1], web3.toWei(2)); - await Promise.all([ - rates.setRate("EUR", 0), - testHelpers.waitForTimeStamp((await loanManager.loans(loan.id))[8].toNumber()) + it("Should not repay with invalid loanId", async function() { + const loanCount = await loanManager.getLoanCount(); + await testHelpers.expectThrow(augmintToken.transferAndNotify(loanManager.address, 10000, loanCount)); + }); + + it("Should NOT repay a loan after maturity", async function() { + const borrower = accounts[0]; + const loan = await loanTestHelpers.createLoan(this, products.defaulting, borrower, web3.toWei(0.5)); + + const [accBal] = await Promise.all([ + augmintToken.balanceOf(borrower), + testHelpers.waitForTimeStamp(loan.maturity + 1) ]); + assert(accBal.gt(loan.repaymentAmount)); // sanitiy to make sure repayment is not failing on insufficient A€ - testHelpers.expectThrow(loanTestHelpers.collectLoan(this, loan, accounts[2])); + await testHelpers.expectThrow( + augmintToken.transferAndNotify(loanManager.address, loan.repaymentAmount, loan.id, { + from: borrower + }) + ); }); - it("Should get loans from offset"); // contract func to be implemented - it("Should get loans for one account from offset"); // contract func to be implemented + it("Should not get a loan when rates = 0", async function() { + await rates.setRate("EUR", 0); + await testHelpers.expectThrow( + loanManager.newEthBackedLoan(products.repaying.id, { + from: accounts[1], + value: web3.toWei(0.1) + }) + ); + await rates.setRate("EUR", 99800); // restore rates + }); + + it("Should list loans from offset", async function() { + const product = products.repaying; + const product2 = products.defaulting; + + const loan1 = await loanTestHelpers.createLoan(this, product, accounts[1], web3.toWei(2)); + const loan2 = await loanTestHelpers.createLoan(this, product2, accounts[2], web3.toWei(0.3)); - it("Should NOT repay a loan after paymentperiod is over"); + await testHelpers.waitForTimeStamp(loan2.maturity); - it("Should NOT collect an already collected ACE loan"); + const loansArray = await loanManager.getLoans(loan1.id); + const loanInfo = loanTestHelpers.parseLoansInfo(loansArray); - it("Should collect multiple defaulted ACE loans "); + assert.equal(loanInfo.length, 2); // offset was from first loan added - it("Should get and repay a loan with colletaralRatio = 1"); - it("Should get and repay a loan with colletaralRatio > 1"); - it("Should get and collect a loan with colletaralRatio = 1"); - it("Should get and collect a loan with colletaralRatio > 1"); - it("Should not get a loan when rates = 0"); + const loan1Actual = loanInfo[0]; - it("Should get a loan if interest rate is negative "); // to be implemented + assert.equal(loan1Actual.id.toNumber(), loan1.id); + assert.equal(loan1Actual.collateralAmount.toNumber(), loan1.collateralAmount); + assert.equal(loan1Actual.repaymentAmount.toNumber(), loan1.repaymentAmount); + assert.equal("0x" + loan1Actual.borrower.toString(16), loan1.borrower); + assert.equal(loan1Actual.productId.toNumber(), product.id); + assert.equal(loan1Actual.state.toNumber(), loan1.state); + assert.equal(loan1Actual.maturity.toNumber(), loan1.maturity); + assert.equal(loan1Actual.disbursementTime.toNumber(), loan1.maturity - product.term); + assert.equal(loan1Actual.loanAmount.toNumber(), loan1.loanAmount); + assert.equal(loan1Actual.interestAmount.toNumber(), loan1.interestAmount); + + const loan2Actual = loanInfo[1]; + + assert.equal(loan2Actual.id.toNumber(), loan2.id); + assert.equal("0x" + loan2Actual.borrower.toString(16), loan2.borrower); + assert.equal(loan2Actual.state.toNumber(), 2); // Defaulted (not collected) + }); + + it("Should list loans for one account from offset", async function() { + const product1 = products.repaying; + const product2 = products.defaulting; + const borrower = accounts[1]; + + const loan1 = await loanTestHelpers.createLoan(this, product1, borrower, web3.toWei(0.2)); + const loan2 = await loanTestHelpers.createLoan(this, product2, borrower, web3.toWei(0.3)); + const accountLoanCount = await loanManager.getLoanCountForAddress(borrower); + + await testHelpers.waitForTimeStamp(loan2.maturity); + + const loansArray = await loanManager.getLoansForAddress(borrower, accountLoanCount - 2); + const loanInfo = loanTestHelpers.parseLoansInfo(loansArray); + assert.equal(loanInfo.length, 2); // offset was from first loan added for account + + const loan1Actual = loanInfo[0]; + + assert.equal(loan1Actual.id.toNumber(), loan1.id); + assert.equal(loan1Actual.collateralAmount.toNumber(), loan1.collateralAmount); + assert.equal(loan1Actual.repaymentAmount.toNumber(), loan1.repaymentAmount); + assert.equal("0x" + loan1Actual.borrower.toString(16), loan1.borrower); + assert.equal(loan1Actual.productId.toNumber(), product1.id); + assert.equal(loan1Actual.state.toNumber(), loan1.state); + assert.equal(loan1Actual.maturity.toNumber(), loan1.maturity); + assert.equal(loan1Actual.disbursementTime.toNumber(), loan1.maturity - product1.term); + assert.equal(loan1Actual.loanAmount.toNumber(), loan1.loanAmount); + assert.equal(loan1Actual.interestAmount.toNumber(), loan1.interestAmount); + + const loan2Actual = loanInfo[1]; + assert.equal(loan2Actual.id.toNumber(), loan2.id); + assert.equal(loan2Actual.state.toNumber(), 2); // Defaulted (not collected) + }); it("should only allow whitelisted loan contract to be used", async function() { const interestEarnedAcc = await monetarySupervisor.interestEarnedAccount(); @@ -189,8 +286,4 @@ contract("Augmint Loans tests", accounts => { it("only allowed contract should call MonetarySupervisor.loanRepaymentNotification", async function() { await testHelpers.expectThrow(monetarySupervisor.loanRepaymentNotification(0, { from: accounts[0] })); }); - - it("only allowed contract should call MonetarySupervisor.loanCollectionNotification", async function() { - await testHelpers.expectThrow(monetarySupervisor.loanCollectionNotification(0, { from: accounts[0] })); - }); }); diff --git a/truffle.js b/truffle.js index 3c48d2b0..d6a76a0b 100644 --- a/truffle.js +++ b/truffle.js @@ -14,7 +14,7 @@ module.exports = { port: 8545, network_id: "999", gas: 4707806, - gasPrice: 1 + gasPrice: 1000000000 // 1 GWEI }, truffleLocal: { host: "localhost", @@ -34,7 +34,7 @@ module.exports = { from: "0xae653250B4220835050B75D3bC91433246903A95", // default address to use for any transaction Truffle makes during migrations network_id: 4, gas: 4700000, // Gas limit used for deploys - gasPrice: 20000000000 // 20 Gwei + gasPrice: 1000000000 // 1 Gwei }, ropsten: { host: "localhost", // Connect to geth on the specified