From f062d566889ce7b27cb0449cfff877f7f32dda83 Mon Sep 17 00:00:00 2001 From: Peter Petrovics Date: Sat, 24 Feb 2018 10:45:24 +0000 Subject: [PATCH 01/20] optimise loanManager step 1: refactor LoanData struct for less storage --- contracts/LoanManager.sol | 116 +++++++++++++++++++------------- test/helpers/loanTestHelpers.js | 37 ++++------ test/loans.js | 10 +-- 3 files changed, 86 insertions(+), 77 deletions(-) diff --git a/contracts/LoanManager.sol b/contracts/LoanManager.sol index bb50d824..171b8187 100644 --- a/contracts/LoanManager.sol +++ b/contracts/LoanManager.sol @@ -34,17 +34,14 @@ contract LoanManager is Restricted { 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; @@ -57,16 +54,16 @@ contract LoanManager is Restricted { MonetarySupervisor public monetarySupervisor; InterestEarnedAccount public interestEarnedAccount; - event NewLoan(uint productId, uint loanId, address borrower, uint collateralAmount, uint loanAmount, + event NewLoan(uint32 productId, uint loanId, address indexed borrower, uint collateralAmount, uint loanAmount, uint repaymentAmount); - 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, @@ -79,23 +76,26 @@ contract LoanManager is Restricted { } function addLoanProduct(uint _term, uint _discountRate, uint _collateralRatio, uint _minDisbursedAmount, - uint _defaultingFee, bool _isActive) - external restrict("MonetaryBoard") returns (uint newProductId) { - newProductId = products.push( + uint _defaultingFee, bool _isActive) + external restrict("MonetaryBoard") { + + uint _newProductId = products.push( LoanProduct(_term, _discountRate, _collateralRatio, _minDisbursedAmount, _defaultingFee, _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? @@ -103,17 +103,18 @@ contract LoanManager is Restricted { uint tokenValue = rates.convertFromWei(augmintToken.peggedSymbol(), msg.value); uint repaymentAmount = tokenValue.mul(product.collateralRatio).roundedDiv(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); @@ -133,32 +134,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]; - totalLoanAmountCollected = totalLoanAmountCollected.add(loans[loanId].loanAmount); + uint loanAmount; + (loanAmount, ) = calculateLoanValues(product, loan.repaymentAmount); - loans[loanId].state = LoanState.Defaulted; + totalLoanAmountCollected = totalLoanAmountCollected.add(loanAmount); + + loan.state = LoanState.Defaulted; // 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) { @@ -193,21 +199,35 @@ 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).roundedDiv(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); + augmintToken.transfer(interestEarnedAccount, interestAmount); - augmintToken.burn(loans[loanId].loanAmount); - monetarySupervisor.loanRepaymentNotification(loans[loanId].loanAmount); // update KPIs + augmintToken.burn(loanAmount); + 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..2b228e62 100644 --- a/test/helpers/loanTestHelpers.js +++ b/test/helpers/loanTestHelpers.js @@ -348,30 +348,19 @@ 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.collateral.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[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/loans.js b/test/loans.js index 75061c5b..389090d1 100644 --- a/test/loans.js +++ b/test/loans.js @@ -82,7 +82,7 @@ contract("Augmint Loans tests", accounts => { 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)); - await testHelpers.waitForTimeStamp((await loanManager.loans(loan.id))[8].toNumber()); + await testHelpers.waitForTimeStamp(loan.product.term.add(loan.disbursementTime).toNumber()); await loanTestHelpers.collectLoan(this, loan, accounts[2]); }); @@ -93,7 +93,7 @@ contract("Augmint Loans tests", accounts => { await Promise.all([ rates.setRate("EUR", 99000), - testHelpers.waitForTimeStamp((await loanManager.loans(loan.id))[8].toNumber()) + testHelpers.waitForTimeStamp(loan.product.term.add(loan.disbursementTime).toNumber()) ]); await loanTestHelpers.collectLoan(this, loan, accounts[2]); @@ -105,7 +105,7 @@ contract("Augmint Loans tests", accounts => { await Promise.all([ rates.setRate("EUR", 98900), - testHelpers.waitForTimeStamp((await loanManager.loans(loan.id))[8].toNumber()) + testHelpers.waitForTimeStamp(loan.product.term.add(loan.disbursementTime).toNumber()) ]); await loanTestHelpers.collectLoan(this, loan, accounts[2]); @@ -116,7 +116,7 @@ contract("Augmint Loans tests", accounts => { 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()) + testHelpers.waitForTimeStamp(loan.product.term.add(loan.disbursementTime).toNumber()) ]); await loanTestHelpers.collectLoan(this, loan, accounts[2]); @@ -127,7 +127,7 @@ contract("Augmint Loans tests", accounts => { 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()) + testHelpers.waitForTimeStamp(loan.product.term.add(loan.disbursementTime).toNumber()) ]); testHelpers.expectThrow(loanTestHelpers.collectLoan(this, loan, accounts[2])); From 822dbca72668c7e9721fd3466b44c3cf0255944c Mon Sep 17 00:00:00 2001 From: Peter Petrovics Date: Sat, 24 Feb 2018 10:59:13 +0000 Subject: [PATCH 02/20] ppm & term values as uint32 --- contracts/LoanManager.sol | 20 ++++++++++---------- test/helpers/loanTestHelpers.js | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/contracts/LoanManager.sol b/contracts/LoanManager.sol index 171b8187..fa6f310e 100644 --- a/contracts/LoanManager.sol +++ b/contracts/LoanManager.sol @@ -25,13 +25,13 @@ contract LoanManager is Restricted { enum LoanState { Open, Repaid, Defaulted } 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) */ @@ -75,12 +75,12 @@ contract LoanManager is Restricted { interestEarnedAccount = _interestEarnedAccount; } - function addLoanProduct(uint _term, uint _discountRate, uint _collateralRatio, uint _minDisbursedAmount, - uint _defaultingFee, bool _isActive) + function addLoanProduct(uint32 term, uint32 discountRate, uint32 collateralRatio, uint minDisbursedAmount, + uint32 defaultingFeePt, bool isActive) external restrict("MonetaryBoard") { uint _newProductId = products.push( - LoanProduct(_term, _discountRate, _collateralRatio, _minDisbursedAmount, _defaultingFee, _isActive) + LoanProduct(minDisbursedAmount, term, discountRate, collateralRatio, defaultingFeePt, isActive) ) - 1; uint32 newProductId = uint32(_newProductId); diff --git a/test/helpers/loanTestHelpers.js b/test/helpers/loanTestHelpers.js index 2b228e62..2218c5e5 100644 --- a/test/helpers/loanTestHelpers.js +++ b/test/helpers/loanTestHelpers.js @@ -309,10 +309,10 @@ 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], + minDisbursedAmount: prod[0], + term: prod[1], + discountRate: prod[2], + collateralRatio: prod[3], defaultingFeePt: prod[4], isActive: prod[5] }; From ffade9459ec14ee0f857af1e3a0e1fb00b6515ea Mon Sep 17 00:00:00 2001 From: Peter Petrovics Date: Sat, 24 Feb 2018 11:09:51 +0000 Subject: [PATCH 03/20] reduce expected gas for new loan & repay loan --- test/helpers/loanTestHelpers.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/helpers/loanTestHelpers.js b/test/helpers/loanTestHelpers.js index 2218c5e5..0c282e62 100644 --- a/test/helpers/loanTestHelpers.js +++ b/test/helpers/loanTestHelpers.js @@ -7,8 +7,8 @@ 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 = 210000; +const REPAY_MAX_GAS = 120000; const COLLECT_BASE_GAS = 100000; let augmintToken = null; From 168e36fd84c9fbe0e0c99624425f657eab8c9c7b Mon Sep 17 00:00:00 2001 From: Peter Petrovics Date: Sat, 24 Feb 2018 21:59:14 +0000 Subject: [PATCH 04/20] replace ACE leftover to A-EUR in test case names --- test/loans.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/test/loans.js b/test/loans.js index 389090d1..192f9818 100644 --- a/test/loans.js +++ b/test/loans.js @@ -52,7 +52,7 @@ contract("Augmint Loans tests", accounts => { ]); }); - 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)); }); @@ -65,21 +65,21 @@ 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 an A-EUR loan on maturity if A-EUR 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 }); - it("Should collect a defaulted ACE loan and send back leftover collateral ", async function() { + 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.product.term.add(loan.disbursementTime).toNumber()); @@ -87,7 +87,7 @@ contract("Augmint Loans tests", accounts => { await loanTestHelpers.collectLoan(this, loan, accounts[2]); }); - it("Should collect a defaulted ACE loan when no leftover collateral (collection exactly covered)", async function() { + 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)); @@ -99,7 +99,7 @@ contract("Augmint Loans tests", accounts => { await loanTestHelpers.collectLoan(this, loan, accounts[2]); }); - it("Should collect a defaulted ACE loan when no leftover collateral (collection partially covered)", async function() { + 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(1)); @@ -111,7 +111,7 @@ contract("Augmint Loans tests", accounts => { await loanTestHelpers.collectLoan(this, loan, accounts[2]); }); - it("Should collect a defaulted ACE loan when no leftover collateral (only fee covered)", async function() { + 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([ @@ -138,9 +138,9 @@ contract("Augmint Loans tests", accounts => { it("Should NOT repay a loan after paymentperiod is over"); - it("Should NOT collect an already collected ACE loan"); + it("Should NOT collect an already collected A-EUR loan"); - it("Should collect multiple defaulted ACE loans "); + it("Should collect multiple defaulted A-EUR loans "); it("Should get and repay a loan with colletaralRatio = 1"); it("Should get and repay a loan with colletaralRatio > 1"); From 261b5ee5a764680b8bc0d2f3e3e95be5f317729e Mon Sep 17 00:00:00 2001 From: Peter Petrovics Date: Sat, 24 Feb 2018 22:11:02 +0000 Subject: [PATCH 05/20] getProducts() in CHUNKS UI helper fx --- contracts/LoanManager.sol | 24 ++++++++++-- test/helpers/loanTestHelpers.js | 33 +++++++++------- test/loanManager.js | 68 +++++++++++++++++++++++++++++---- test/loans.js | 22 +++++------ 4 files changed, 109 insertions(+), 38 deletions(-) diff --git a/contracts/LoanManager.sol b/contracts/LoanManager.sol index fa6f310e..80324499 100644 --- a/contracts/LoanManager.sol +++ b/contracts/LoanManager.sol @@ -22,6 +22,8 @@ import "./MonetarySupervisor.sol"; contract LoanManager is Restricted { using SafeMath for uint256; + uint16 public constant CHUNK_SIZE = 100; + enum LoanState { Open, Repaid, Defaulted } struct LoanProduct { @@ -175,14 +177,28 @@ contract LoanManager is Restricted { } - function getLoanCount() external view returns (uint ct) { - return loans.length; - } - 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 getLoanIds(address borrower) external view returns (uint[] _loans) { return mLoans[borrower]; } diff --git a/test/helpers/loanTestHelpers.js b/test/helpers/loanTestHelpers.js index 0c282e62..cfc73119 100644 --- a/test/helpers/loanTestHelpers.js +++ b/test/helpers/loanTestHelpers.js @@ -11,6 +11,8 @@ const NEWLOAN_MAX_GAS = 210000; 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,20 @@ module.exports = { createLoan, repayLoan, collectLoan, - getProductInfo, + getProductsInfo, 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; @@ -68,7 +74,7 @@ async function createLoan(testInstance, product, borrower, collateralWei) { 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(), loanAmount: loan.loanAmount.toString(), @@ -305,18 +311,17 @@ async function collectLoan(testInstance, loan, collector) { ); } -async function getProductInfo(productId) { - const prod = await loanManager.products(productId); - const info = { - id: productId, - minDisbursedAmount: prod[0], - term: prod[1], - discountRate: prod[2], - collateralRatio: 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; } async function calcLoanValues(rates, product, collateralWei) { 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 192f9818..b4f5e885 100644 --- a/test/loans.js +++ b/test/loans.js @@ -35,21 +35,17 @@ contract("Augmint Loans tests", accounts => { // 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), + const [newProducts] = await Promise.all([ + loanTestHelpers.getProductsInfo(prodCount), tokenTestHelpers.withdrawFromReserve(accounts[0], 1000000000) ]); + [ + products.notDue, + products.repaying, + products.defaulting, + products.defaultingNoLeftOver, + products.disabledProduct + ] = newProducts; }); it("Should get an A-EUR loan", async function() { From fb07318e451947a1d588d018c736bde33498a116 Mon Sep 17 00:00:00 2001 From: Peter Petrovics Date: Sat, 24 Feb 2018 22:20:03 +0000 Subject: [PATCH 06/20] rename mLoans to accountLoans --- contracts/LoanManager.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/LoanManager.sol b/contracts/LoanManager.sol index 80324499..ac94b135 100644 --- a/contracts/LoanManager.sol +++ b/contracts/LoanManager.sol @@ -49,7 +49,7 @@ contract LoanManager is Restricted { 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 @@ -119,7 +119,7 @@ contract LoanManager is Restricted { 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); @@ -200,7 +200,7 @@ contract LoanManager is Restricted { } function getLoanIds(address borrower) external view returns (uint[] _loans) { - return mLoans[borrower]; + return accountLoans[borrower]; } /* repay loan, called from AugmintToken's transferAndNotify From 4246012478e31cef538c0a1cf1c761642825d3f3 Mon Sep 17 00:00:00 2001 From: Peter Petrovics Date: Sat, 24 Feb 2018 22:58:53 +0000 Subject: [PATCH 07/20] rename collateral to collateralAmount in test (in sync with LoanData) --- test/helpers/loanTestHelpers.js | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/test/helpers/loanTestHelpers.js b/test/helpers/loanTestHelpers.js index cfc73119..07c15b59 100644 --- a/test/helpers/loanTestHelpers.js +++ b/test/helpers/loanTestHelpers.js @@ -67,7 +67,7 @@ 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"); @@ -76,7 +76,7 @@ async function createLoan(testInstance, product, borrower, collateralWei) { loanId: x => x, productId: loan.product.id.toNumber(), borrower: loan.borrower, - collateralAmount: loan.collateral.toString(), + collateralAmount: loan.collateralAmount.toString(), loanAmount: loan.loanAmount.toString(), repaymentAmount: loan.repaymentAmount.toString() }), @@ -109,11 +109,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: {} }) @@ -179,11 +179,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) @@ -230,14 +230,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"); @@ -246,7 +246,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 @@ -296,7 +296,7 @@ async function collectLoan(testInstance, loan, collector) { }, loanManager: { - eth: balBefore.loanManager.eth.minus(loan.collateral) + eth: balBefore.loanManager.eth.minus(loan.collateralAmount) }, interestEarned: {} @@ -328,7 +328,7 @@ 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 @@ -353,7 +353,7 @@ async function calcLoanValues(rates, product, collateralWei) { async function loanAsserts(expLoan) { const loan = await loanManager.loans(expLoan.id); - assert.equal(loan[0].toString(), expLoan.collateral.toString(), "collateralAmount should be set"); + 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"); From 13be0ca14b62792e26082e7035c8df62835f4ac8 Mon Sep 17 00:00:00 2001 From: Peter Petrovics Date: Sat, 24 Feb 2018 23:17:58 +0000 Subject: [PATCH 08/20] added maturity to NewLoan event --- contracts/LoanManager.sol | 4 ++-- test/helpers/loanTestHelpers.js | 4 +++- test/loans.js | 22 +++++----------------- 3 files changed, 10 insertions(+), 20 deletions(-) diff --git a/contracts/LoanManager.sol b/contracts/LoanManager.sol index ac94b135..9216a852 100644 --- a/contracts/LoanManager.sol +++ b/contracts/LoanManager.sol @@ -57,7 +57,7 @@ contract LoanManager is Restricted { InterestEarnedAccount public interestEarnedAccount; event NewLoan(uint32 productId, uint loanId, address indexed borrower, uint collateralAmount, uint loanAmount, - uint repaymentAmount); + uint repaymentAmount, uint40 maturity); event LoanProductActiveStateChanged(uint32 productId, bool newState); @@ -124,7 +124,7 @@ contract LoanManager is Restricted { // 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 { diff --git a/test/helpers/loanTestHelpers.js b/test/helpers/loanTestHelpers.js index 07c15b59..b3fbea61 100644 --- a/test/helpers/loanTestHelpers.js +++ b/test/helpers/loanTestHelpers.js @@ -78,7 +78,8 @@ async function createLoan(testInstance, product, borrower, collateralWei) { borrower: loan.borrower, collateralAmount: loan.collateralAmount.toString(), loanAmount: loan.loanAmount.toString(), - repaymentAmount: loan.repaymentAmount.toString() + repaymentAmount: loan.repaymentAmount.toString(), + maturity: x => x }), testHelpers.assertEvent(augmintToken, "AugmintTransfer", { @@ -98,6 +99,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(), diff --git a/test/loans.js b/test/loans.js index b4f5e885..99f808fa 100644 --- a/test/loans.js +++ b/test/loans.js @@ -78,7 +78,7 @@ contract("Augmint Loans tests", accounts => { 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.product.term.add(loan.disbursementTime).toNumber()); + await testHelpers.waitForTimeStamp(loan.maturity); await loanTestHelpers.collectLoan(this, loan, accounts[2]); }); @@ -87,10 +87,7 @@ contract("Augmint Loans tests", accounts => { 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.product.term.add(loan.disbursementTime).toNumber()) - ]); + await Promise.all([rates.setRate("EUR", 99000), testHelpers.waitForTimeStamp(loan.maturity)]); await loanTestHelpers.collectLoan(this, loan, accounts[2]); }); @@ -99,10 +96,7 @@ contract("Augmint Loans tests", accounts => { await rates.setRate("EUR", 100000); const loan = await loanTestHelpers.createLoan(this, products.defaultingNoLeftOver, accounts[1], web3.toWei(1)); - await Promise.all([ - rates.setRate("EUR", 98900), - testHelpers.waitForTimeStamp(loan.product.term.add(loan.disbursementTime).toNumber()) - ]); + await Promise.all([rates.setRate("EUR", 98900), testHelpers.waitForTimeStamp(loan.maturity)]); await loanTestHelpers.collectLoan(this, loan, accounts[2]); }); @@ -110,10 +104,7 @@ contract("Augmint Loans tests", accounts => { 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.product.term.add(loan.disbursementTime).toNumber()) - ]); + await Promise.all([rates.setRate("EUR", 1), testHelpers.waitForTimeStamp(loan.maturity)]); await loanTestHelpers.collectLoan(this, loan, accounts[2]); }); @@ -121,10 +112,7 @@ contract("Augmint Loans tests", accounts => { 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.product.term.add(loan.disbursementTime).toNumber()) - ]); + await Promise.all([rates.setRate("EUR", 0), testHelpers.waitForTimeStamp(loan.maturity)]); testHelpers.expectThrow(loanTestHelpers.collectLoan(this, loan, accounts[2])); }); From 11f5e04f7c836c2b34cd89763c23a609077c9c57 Mon Sep 17 00:00:00 2001 From: Peter Petrovics Date: Sat, 24 Feb 2018 23:39:31 +0000 Subject: [PATCH 09/20] getLoans() UI helper fx --- contracts/LoanManager.sol | 22 ++++++++++++++++-- test/helpers/loanTestHelpers.js | 41 ++++++++++++++++++++++++++++++++- test/loans.js | 22 +++++++++++++++++- 3 files changed, 81 insertions(+), 4 deletions(-) diff --git a/contracts/LoanManager.sol b/contracts/LoanManager.sol index 9216a852..c515e83b 100644 --- a/contracts/LoanManager.sol +++ b/contracts/LoanManager.sol @@ -199,8 +199,26 @@ contract LoanManager is Restricted { return loans.length; } - function getLoanIds(address borrower) external view returns (uint[] _loans) { - return accountLoans[borrower]; + /* 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; } + + LoanData storage loan = loans[offset + i]; + LoanProduct storage product = products[loan.productId]; + + uint loanAmount; + uint interestAmount; + (loanAmount, interestAmount) = calculateLoanValues(product, loan.repaymentAmount); + uint disbursementTime = loan.maturity - product.term; + + response[i] = [offset + i, loan.collateralAmount, loan.repaymentAmount, uint(loan.borrower), + loan.productId, uint(loan.state), loan.maturity, disbursementTime, loanAmount, interestAmount]; + } } /* repay loan, called from AugmintToken's transferAndNotify diff --git a/test/helpers/loanTestHelpers.js b/test/helpers/loanTestHelpers.js index b3fbea61..1b153d6a 100644 --- a/test/helpers/loanTestHelpers.js +++ b/test/helpers/loanTestHelpers.js @@ -7,7 +7,7 @@ const Rates = artifacts.require("./Rates.sol"); const tokenTestHelpers = require("./tokenTestHelpers.js"); const testHelpers = require("./testHelpers.js"); -const NEWLOAN_MAX_GAS = 210000; +const NEWLOAN_MAX_GAS = 220000; const REPAY_MAX_GAS = 120000; const COLLECT_BASE_GAS = 100000; @@ -26,6 +26,7 @@ module.exports = { repayLoan, collectLoan, getProductsInfo, + getLoansInfo, calcLoanValues, loanAsserts, get loanManager() { @@ -326,6 +327,43 @@ async function getProductsInfo(offset) { return result; } +async function getLoansInfo(offset) { + const loans = await loanManager.getLoans(offset); + 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; @@ -360,6 +398,7 @@ async function loanAsserts(expLoan) { 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"); const maturityActual = loan[5]; const maturityExpected = expLoan.product.term.add(expLoan.disbursementTime).toNumber(); diff --git a/test/loans.js b/test/loans.js index 99f808fa..bc8a1fea 100644 --- a/test/loans.js +++ b/test/loans.js @@ -115,9 +115,29 @@ contract("Augmint Loans tests", accounts => { 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 list loans from offset", async function() { + const product = products.repaying; + + const loan = await loanTestHelpers.createLoan(this, product, accounts[1], web3.toWei(2)); + + const loanInfo = await loanTestHelpers.getLoansInfo(loan.id); + const lastLoan = loanInfo[0]; + + assert.equal(lastLoan.id.toNumber(), loan.id); + assert.equal(lastLoan.collateralAmount.toNumber(), loan.collateralAmount); + assert.equal(lastLoan.repaymentAmount.toNumber(), loan.repaymentAmount); + assert.equal("0x" + lastLoan.borrower.toString(16), loan.borrower); + assert.equal(lastLoan.productId.toNumber(), product.id); + assert.equal(lastLoan.state.toNumber(), loan.state); + assert.equal(lastLoan.maturity.toNumber(), loan.maturity); + assert.equal(lastLoan.disbursementTime.toNumber(), loan.maturity - product.term); + assert.equal(lastLoan.loanAmount.toNumber(), loan.loanAmount); + assert.equal(lastLoan.interestAmount.toNumber(), loan.interestAmount); }); - 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 repay a loan after paymentperiod is over"); From 00ef3e7ab622b0c7fa30d351908d98c985fb72bc Mon Sep 17 00:00:00 2001 From: Peter Petrovics Date: Sun, 25 Feb 2018 00:02:42 +0000 Subject: [PATCH 10/20] getLoanCountForAddress and getLoansForAddress UI helpers --- contracts/LoanManager.sol | 30 ++++++++++++++++++++++++++++++ test/helpers/loanTestHelpers.js | 6 +++--- test/loans.js | 31 +++++++++++++++++++++++++++++-- 3 files changed, 62 insertions(+), 5 deletions(-) diff --git a/contracts/LoanManager.sol b/contracts/LoanManager.sol index c515e83b..9edc06a2 100644 --- a/contracts/LoanManager.sol +++ b/contracts/LoanManager.sol @@ -221,6 +221,36 @@ contract LoanManager is Restricted { } } + 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; } + + uint loanId = loansForAddress[offset + i]; + + 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; + + response[i] = [loanId, loan.collateralAmount, loan.repaymentAmount, uint(loan.borrower), + loan.productId, uint(loan.state), loan.maturity, disbursementTime, loanAmount, interestAmount]; + } + } + /* repay loan, called from AugmintToken's transferAndNotify Flow for repaying loan: 1) user calls token contract's transferAndNotify loanId passed in data arg diff --git a/test/helpers/loanTestHelpers.js b/test/helpers/loanTestHelpers.js index 1b153d6a..bb53d6bc 100644 --- a/test/helpers/loanTestHelpers.js +++ b/test/helpers/loanTestHelpers.js @@ -26,7 +26,7 @@ module.exports = { repayLoan, collectLoan, getProductsInfo, - getLoansInfo, + parseLoansInfo, calcLoanValues, loanAsserts, get loanManager() { @@ -327,8 +327,8 @@ async function getProductsInfo(offset) { return result; } -async function getLoansInfo(offset) { - const loans = await loanManager.getLoans(offset); +/* parse array returned by getLoans & getLoansForAddress */ +function parseLoansInfo(loans) { assert.equal(loans.length, CHUNK_SIZE); const result = []; loans.map(loan => { diff --git a/test/loans.js b/test/loans.js index bc8a1fea..2ce90775 100644 --- a/test/loans.js +++ b/test/loans.js @@ -123,7 +123,11 @@ contract("Augmint Loans tests", accounts => { const loan = await loanTestHelpers.createLoan(this, product, accounts[1], web3.toWei(2)); - const loanInfo = await loanTestHelpers.getLoansInfo(loan.id); + const loansArray = await loanManager.getLoans(loan.id); + const loanInfo = loanTestHelpers.parseLoansInfo(loansArray); + + assert.equal(loanInfo.length, 1); // offset was from last loan added + const lastLoan = loanInfo[0]; assert.equal(lastLoan.id.toNumber(), loan.id); @@ -138,7 +142,30 @@ contract("Augmint Loans tests", accounts => { assert.equal(lastLoan.interestAmount.toNumber(), loan.interestAmount); }); - it("Should get loans for one account from offset"); // contract func to be implemented + it("Should list loans for one account from offset", async function() { + const product = products.repaying; + const borrower = accounts[1]; + + const loan = await loanTestHelpers.createLoan(this, product, borrower, web3.toWei(2)); + const accountLoanCount = await loanManager.getLoanCountForAddress(borrower); + + const loansArray = await loanManager.getLoansForAddress(borrower, accountLoanCount - 1); + const loanInfo = loanTestHelpers.parseLoansInfo(loansArray); + assert.equal(loanInfo.length, 1); // offset was from last loan added for account + + const lastLoan = loanInfo[0]; + + assert.equal(lastLoan.id.toNumber(), loan.id); + assert.equal(lastLoan.collateralAmount.toNumber(), loan.collateralAmount); + assert.equal(lastLoan.repaymentAmount.toNumber(), loan.repaymentAmount); + assert.equal("0x" + lastLoan.borrower.toString(16), loan.borrower); + assert.equal(lastLoan.productId.toNumber(), product.id); + assert.equal(lastLoan.state.toNumber(), loan.state); + assert.equal(lastLoan.maturity.toNumber(), loan.maturity); + assert.equal(lastLoan.disbursementTime.toNumber(), loan.maturity - product.term); + assert.equal(lastLoan.loanAmount.toNumber(), loan.loanAmount); + assert.equal(lastLoan.interestAmount.toNumber(), loan.interestAmount); + }); it("Should NOT repay a loan after paymentperiod is over"); From 09269c0e6106bef3587c9f99db0a57d0a8b97550 Mon Sep 17 00:00:00 2001 From: Peter Petrovics Date: Sun, 25 Feb 2018 00:11:19 +0000 Subject: [PATCH 11/20] div instead of roundedDiv for loan calculations --- contracts/LoanManager.sol | 4 ++-- test/helpers/loanTestHelpers.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/LoanManager.sol b/contracts/LoanManager.sol index 9edc06a2..580617ca 100644 --- a/contracts/LoanManager.sol +++ b/contracts/LoanManager.sol @@ -103,7 +103,7 @@ contract LoanManager is Restricted { // 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; (loanAmount, ) = calculateLoanValues(product, repaymentAmount); @@ -266,7 +266,7 @@ contract LoanManager is Restricted { 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).roundedDiv(1000000); + loanAmount = repaymentAmount.mul(product.discountRate).div(1000000); interestAmount = loanAmount > repaymentAmount ? 0 : repaymentAmount.sub(loanAmount); } diff --git a/test/helpers/loanTestHelpers.js b/test/helpers/loanTestHelpers.js index bb53d6bc..63ed7a36 100644 --- a/test/helpers/loanTestHelpers.js +++ b/test/helpers/loanTestHelpers.js @@ -374,13 +374,13 @@ async function calcLoanValues(rates, product, 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.minus(ret.loanAmount); ret.disbursementTime = moment() From 21b1b71c58d205ffc24d3ed7d8710a3eaaf0acc5 Mon Sep 17 00:00:00 2001 From: Peter Petrovics Date: Sun, 25 Feb 2018 00:22:55 +0000 Subject: [PATCH 12/20] less agressive solhint config --- contracts/.solhint.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 contracts/.solhint.json diff --git a/contracts/.solhint.json b/contracts/.solhint.json new file mode 100644 index 00000000..bdae247c --- /dev/null +++ b/contracts/.solhint.json @@ -0,0 +1,11 @@ +{ + "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" + } +} From 8ff5edb0ea917605f3572fe600d6bdf3997e9dcd Mon Sep 17 00:00:00 2001 From: Peter Petrovics Date: Sun, 25 Feb 2018 18:53:53 +0000 Subject: [PATCH 13/20] finished a few pending tests --- test/loans.js | 124 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 110 insertions(+), 14 deletions(-) diff --git a/test/loans.js b/test/loans.js index 2ce90775..740e284b 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; @@ -52,7 +53,20 @@ contract("Augmint Loans tests", accounts => { 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( @@ -60,21 +74,95 @@ contract("Augmint Loans tests", accounts => { ); }); - it("Should NOT collect a loan before it's due"); - it("Should NOT repay an A-EUR loan on maturity if A-EUR 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 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 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, true); + }); + + 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 augmintToken.transferAndNotify(loanManager.address, loan.repaymentAmount, loan.id, { + from: accounts[0] + }); + + await testHelpers.assertEvent(loanManager, "LoanRepayed", { + loanId: loan.id, + borrower: loan.borrower + }); + }); + + 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); + + // 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 testHelpers.expectThrow( + augmintToken.transferAndNotify(loanManager.address, loan.repaymentAmount, loan.id, { from: borrower }) + ); }); + 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 testHelpers.expectThrow( + augmintToken.transferAndNotify(loanManager.address, loan.repaymentAmount.sub(1), loan.id, { + from: borrower + }) + ); + }); + + 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€ + + await testHelpers.expectThrow( + augmintToken.transferAndNotify(loanManager.address, loan.repaymentAmount, loan.id, { + from: borrower + }) + ); + }); + + 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 get a loan if interest rate is negative "); // to be implemented + 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)); @@ -107,6 +195,12 @@ contract("Augmint Loans tests", accounts => { 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 NOT collect a loan before it's due", async function() { + const loan = await loanTestHelpers.createLoan(this, products.repaying, accounts[1], web3.toWei(0.5)); + await testHelpers.expectThrow(loanManager.collect([loan.id])); }); it("Should not collect when rate = 0", async function() { @@ -167,9 +261,14 @@ contract("Augmint Loans tests", accounts => { assert.equal(lastLoan.interestAmount.toNumber(), loan.interestAmount); }); - it("Should NOT repay a loan after paymentperiod is over"); + it("Should NOT collect an already collected loan", async function() { + const loan = await loanTestHelpers.createLoan(this, products.defaulting, accounts[1], web3.toWei(0.5)); - it("Should NOT collect an already collected A-EUR loan"); + await testHelpers.waitForTimeStamp(loan.maturity); + + await loanTestHelpers.collectLoan(this, loan, accounts[2]); + await testHelpers.expectThrow(loanManager.collect([loan.id])); + }); it("Should collect multiple defaulted A-EUR loans "); @@ -177,9 +276,6 @@ contract("Augmint Loans tests", accounts => { 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"); - - it("Should get a loan if interest rate is negative "); // to be implemented it("should only allow whitelisted loan contract to be used", async function() { const interestEarnedAcc = await monetarySupervisor.interestEarnedAccount(); From 692dd973cb60ba6254c0f95123b08b9e0cbc51aa Mon Sep 17 00:00:00 2001 From: Peter Petrovics Date: Sun, 25 Feb 2018 19:09:53 +0000 Subject: [PATCH 14/20] split loanCollection tests from loans --- test/loanCollection.js | 98 ++++++++++++++++++++++++++++++++++++++++++ test/loans.js | 94 +++------------------------------------- 2 files changed, 105 insertions(+), 87 deletions(-) create mode 100644 test/loanCollection.js diff --git a/test/loanCollection.js b/test/loanCollection.js new file mode 100644 index 00000000..375e1da4 --- /dev/null +++ b/test/loanCollection.js @@ -0,0 +1,98 @@ +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 + + const [newProducts] = await Promise.all([ + loanTestHelpers.getProductsInfo(prodCount), + tokenTestHelpers.withdrawFromReserve(accounts[0], 1000000000) + ]); + [products.notDue, products.defaulting, products.defaultingNoLeftOver] = 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(1)); + + 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 colletaralRatio = 1"); + it("Should get and collect a loan with colletaralRatio > 1"); + + it("Should collect multiple defaulted A-EUR loans "); + + 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/loans.js b/test/loans.js index 740e284b..8ec63411 100644 --- a/test/loans.js +++ b/test/loans.js @@ -20,33 +20,19 @@ contract("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); + // 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 const [newProducts] = await Promise.all([ loanTestHelpers.getProductsInfo(prodCount), tokenTestHelpers.withdrawFromReserve(accounts[0], 1000000000) ]); - [ - products.notDue, - products.repaying, - products.defaulting, - products.defaultingNoLeftOver, - products.disabledProduct - ] = newProducts; + [products.notDue, products.repaying, products.defaulting, products.disabledProduct] = newProducts; }); it("Should get an A-EUR loan", async function() { @@ -163,55 +149,6 @@ contract("Loans tests", accounts => { it("Should get a loan if interest rate is negative "); // to be implemented - 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(1)); - - 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 NOT collect a loan before it's due", async function() { - const loan = await loanTestHelpers.createLoan(this, products.repaying, 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 list loans from offset", async function() { const product = products.repaying; @@ -261,21 +198,8 @@ contract("Loans tests", accounts => { assert.equal(lastLoan.interestAmount.toNumber(), loan.interestAmount); }); - 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("Should collect multiple defaulted A-EUR loans "); - 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 only allow whitelisted loan contract to be used", async function() { const interestEarnedAcc = await monetarySupervisor.interestEarnedAccount(); @@ -316,8 +240,4 @@ contract("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] })); - }); }); From 469278d87ebff9b2b789f73be6ed0e48e2a28d0b Mon Sep 17 00:00:00 2001 From: Peter Petrovics Date: Sun, 25 Feb 2018 20:06:09 +0000 Subject: [PATCH 15/20] allow zero or negative interest rate loans --- contracts/LoanManager.sol | 9 +++++++-- test/helpers/loanTestHelpers.js | 12 +++++++++--- test/loanCollection.js | 21 ++++++++++++++++++--- test/loans.js | 23 ++++++++++++++++++++--- 4 files changed, 54 insertions(+), 11 deletions(-) diff --git a/contracts/LoanManager.sol b/contracts/LoanManager.sol index 580617ca..14747b31 100644 --- a/contracts/LoanManager.sol +++ b/contracts/LoanManager.sol @@ -284,9 +284,14 @@ contract LoanManager is Restricted { loans[loanId].state = LoanState.Repaid; - augmintToken.transfer(interestEarnedAccount, 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(loanAmount); monetarySupervisor.loanRepaymentNotification(loanAmount); // update KPIs loan.borrower.transfer(loan.collateralAmount); // send back ETH collateral diff --git a/test/helpers/loanTestHelpers.js b/test/helpers/loanTestHelpers.js index 63ed7a36..3d2dee73 100644 --- a/test/helpers/loanTestHelpers.js +++ b/test/helpers/loanTestHelpers.js @@ -196,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(), @@ -382,7 +385,10 @@ async function calcLoanValues(rates, product, collateralWei) { .div(ppmDiv * ppmDiv) .round(0, BigNumber.ROUND_DOWN); - ret.interestAmount = ret.repaymentAmount.minus(ret.loanAmount); + ret.interestAmount = ret.repaymentAmount.gt(ret.loanAmount) + ? ret.repaymentAmount.minus(ret.loanAmount) + : new BigNumber(0); + ret.disbursementTime = moment() .utc() .unix(); diff --git a/test/loanCollection.js b/test/loanCollection.js index 375e1da4..1f95afd4 100644 --- a/test/loanCollection.js +++ b/test/loanCollection.js @@ -21,12 +21,20 @@ contract("Loans tests", accounts => { 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 const [newProducts] = await Promise.all([ loanTestHelpers.getProductsInfo(prodCount), tokenTestHelpers.withdrawFromReserve(accounts[0], 1000000000) ]); - [products.notDue, products.defaulting, products.defaultingNoLeftOver] = newProducts; + [ + products.notDue, + products.defaulting, + products.defaultingNoLeftOver, + products.zeroInterest, + products.negativeInterest + ] = newProducts; }); it("Should collect a defaulted A-EUR loan and send back leftover collateral ", async function() { @@ -64,8 +72,15 @@ contract("Loans tests", accounts => { await rates.setRate("EUR", 99800); // restore rates }); - it("Should get and collect 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", 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 colletaralRatio > 1", 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 collect multiple defaulted A-EUR loans "); diff --git a/test/loans.js b/test/loans.js index 8ec63411..b396ec1b 100644 --- a/test/loans.js +++ b/test/loans.js @@ -27,12 +27,21 @@ contract("Loans tests", accounts => { 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 const [newProducts] = await Promise.all([ loanTestHelpers.getProductsInfo(prodCount), tokenTestHelpers.withdrawFromReserve(accounts[0], 1000000000) ]); - [products.notDue, products.repaying, products.defaulting, products.disabledProduct] = newProducts; + [ + products.notDue, + products.repaying, + products.defaulting, + products.disabledProduct, + products.zeroInterest, + products.negativeInterest + ] = newProducts; }); it("Should get an A-EUR loan", async function() { @@ -69,6 +78,16 @@ contract("Loans tests", accounts => { await loanTestHelpers.repayLoan(this, loan, true); }); + 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, true); + }); + + 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, true); + }); + 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)); @@ -147,8 +166,6 @@ contract("Loans tests", accounts => { await rates.setRate("EUR", 99800); // restore rates }); - it("Should get a loan if interest rate is negative "); // to be implemented - it("Should list loans from offset", async function() { const product = products.repaying; From 87c95628f76f553046c902a2f7a10b9b2874d47b Mon Sep 17 00:00:00 2001 From: Peter Petrovics Date: Sun, 25 Feb 2018 20:10:00 +0000 Subject: [PATCH 16/20] solhint config statement-indent warn only --- contracts/.solhint.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/.solhint.json b/contracts/.solhint.json index bdae247c..5b6d43e4 100644 --- a/contracts/.solhint.json +++ b/contracts/.solhint.json @@ -6,6 +6,7 @@ "separate-by-one-line-in-contract": "warn", "expression-indent": "warn", "indent": "warn", - "func-order": "warn" + "func-order": "warn", + "statement-indent": "warn" } } From f0dbed1438795384f937b5bb2f84a6ddd235975f Mon Sep 17 00:00:00 2001 From: Peter Petrovics Date: Sun, 25 Feb 2018 20:53:41 +0000 Subject: [PATCH 17/20] finished all pending loans test cases --- test/loanCollection.js | 48 +++++++++++++++++++++++++++++++++++++----- test/loans.js | 25 ++++++++++++++++------ 2 files changed, 61 insertions(+), 12 deletions(-) diff --git a/test/loanCollection.js b/test/loanCollection.js index 1f95afd4..56a2ca8b 100644 --- a/test/loanCollection.js +++ b/test/loanCollection.js @@ -23,6 +23,8 @@ contract("Loans tests", accounts => { 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), @@ -33,7 +35,9 @@ contract("Loans tests", accounts => { products.defaulting, products.defaultingNoLeftOver, products.zeroInterest, - products.negativeInterest + products.negativeInterest, + products.fullCoverage, + products.moreCoverage ] = newProducts; }); @@ -56,7 +60,7 @@ contract("Loans tests", accounts => { 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(1)); + const loan = await loanTestHelpers.createLoan(this, products.defaultingNoLeftOver, accounts[1], web3.toWei(2)); await Promise.all([rates.setRate("EUR", 98900), testHelpers.waitForTimeStamp(loan.maturity)]); @@ -72,17 +76,51 @@ contract("Loans tests", accounts => { await rates.setRate("EUR", 99800); // restore rates }); - it("Should get and collect a loan with colletaralRatio = 1", async function() { + 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 colletaralRatio > 1", async function() { + 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 collect multiple defaulted A-EUR loans "); + 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)); diff --git a/test/loans.js b/test/loans.js index b396ec1b..d0326465 100644 --- a/test/loans.js +++ b/test/loans.js @@ -29,6 +29,8 @@ contract("Loans tests", accounts => { 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), @@ -40,7 +42,9 @@ contract("Loans tests", accounts => { products.defaulting, products.disabledProduct, products.zeroInterest, - products.negativeInterest + products.negativeInterest, + products.fullCoverage, + products.moreCoverage ] = newProducts; }); @@ -75,17 +79,27 @@ contract("Loans tests", accounts => { await augmintToken.transfer(loan.borrower, loan.interestAmount, { from: accounts[0] }); - await loanTestHelpers.repayLoan(this, loan, true); + await loanTestHelpers.repayLoan(this, loan); }); 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, true); + await loanTestHelpers.repayLoan(this, loan); }); 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, true); + await loanTestHelpers.repayLoan(this, loan); + }); + + 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 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); }); it("Non owner should be able to repay a loan too", async function() { @@ -215,9 +229,6 @@ contract("Loans tests", accounts => { assert.equal(lastLoan.interestAmount.toNumber(), loan.interestAmount); }); - it("Should get and repay a loan with colletaralRatio = 1"); - it("Should get and repay a loan with colletaralRatio > 1"); - it("should only allow whitelisted loan contract to be used", async function() { const interestEarnedAcc = await monetarySupervisor.interestEarnedAccount(); const craftedLender = await LoanManager.new( From de224c9fbdbd01d1c58b2746249c835dc82036e7 Mon Sep 17 00:00:00 2001 From: Peter Petrovics Date: Sun, 25 Feb 2018 22:40:46 +0000 Subject: [PATCH 18/20] split Defaulted loanstate to Defaulted (only via getters) and Collected --- contracts/LoanManager.sol | 14 ++++-- test/helpers/loanTestHelpers.js | 10 +---- test/loans.js | 80 ++++++++++++++++++++------------- 3 files changed, 60 insertions(+), 44 deletions(-) diff --git a/contracts/LoanManager.sol b/contracts/LoanManager.sol index 14747b31..61783bcf 100644 --- a/contracts/LoanManager.sol +++ b/contracts/LoanManager.sol @@ -24,7 +24,7 @@ contract LoanManager is Restricted { uint16 public constant CHUNK_SIZE = 100; - enum LoanState { Open, Repaid, Defaulted } + enum LoanState { Open, Repaid, Defaulted, Collected } // NB: Defaulted state is not stored, only getters calculate struct LoanProduct { uint minDisbursedAmount; // 0: with decimals set in AugmintToken.decimals @@ -146,7 +146,7 @@ contract LoanManager is Restricted { totalLoanAmountCollected = totalLoanAmountCollected.add(loanAmount); - loan.state = LoanState.Defaulted; + loan.state = LoanState.Collected; // send ETH collateral to augmintToken reserve uint defaultingFeeInToken = loan.repaymentAmount.mul(product.defaultingFeePt).div(1000000); @@ -216,8 +216,11 @@ contract LoanManager is Restricted { (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; + response[i] = [offset + i, loan.collateralAmount, loan.repaymentAmount, uint(loan.borrower), - loan.productId, uint(loan.state), loan.maturity, disbursementTime, loanAmount, interestAmount]; + loan.productId, uint(loanState), loan.maturity, disbursementTime, loanAmount, interestAmount]; } } @@ -246,8 +249,11 @@ contract LoanManager is Restricted { (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; + response[i] = [loanId, loan.collateralAmount, loan.repaymentAmount, uint(loan.borrower), - loan.productId, uint(loan.state), loan.maturity, disbursementTime, loanAmount, interestAmount]; + loan.productId, uint(loanState), loan.maturity, disbursementTime, loanAmount, interestAmount]; } } diff --git a/test/helpers/loanTestHelpers.js b/test/helpers/loanTestHelpers.js index 3d2dee73..5955fd87 100644 --- a/test/helpers/loanTestHelpers.js +++ b/test/helpers/loanTestHelpers.js @@ -211,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); @@ -263,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(), diff --git a/test/loans.js b/test/loans.js index d0326465..7a0187b2 100644 --- a/test/loans.js +++ b/test/loans.js @@ -182,51 +182,69 @@ contract("Loans tests", accounts => { it("Should list loans from offset", async function() { const product = products.repaying; + const product2 = products.defaulting; - const loan = await loanTestHelpers.createLoan(this, product, accounts[1], web3.toWei(2)); + 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)); - const loansArray = await loanManager.getLoans(loan.id); + await testHelpers.waitForTimeStamp(loan2.maturity); + + const loansArray = await loanManager.getLoans(loan1.id); const loanInfo = loanTestHelpers.parseLoansInfo(loansArray); - assert.equal(loanInfo.length, 1); // offset was from last loan added + assert.equal(loanInfo.length, 2); // offset was from first loan added + + 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(), 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 lastLoan = loanInfo[0]; + const loan2Actual = loanInfo[1]; - assert.equal(lastLoan.id.toNumber(), loan.id); - assert.equal(lastLoan.collateralAmount.toNumber(), loan.collateralAmount); - assert.equal(lastLoan.repaymentAmount.toNumber(), loan.repaymentAmount); - assert.equal("0x" + lastLoan.borrower.toString(16), loan.borrower); - assert.equal(lastLoan.productId.toNumber(), product.id); - assert.equal(lastLoan.state.toNumber(), loan.state); - assert.equal(lastLoan.maturity.toNumber(), loan.maturity); - assert.equal(lastLoan.disbursementTime.toNumber(), loan.maturity - product.term); - assert.equal(lastLoan.loanAmount.toNumber(), loan.loanAmount); - assert.equal(lastLoan.interestAmount.toNumber(), loan.interestAmount); + 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 product = products.repaying; + const product1 = products.repaying; + const product2 = products.defaulting; const borrower = accounts[1]; - const loan = await loanTestHelpers.createLoan(this, product, borrower, web3.toWei(2)); + 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); - const loansArray = await loanManager.getLoansForAddress(borrower, accountLoanCount - 1); + await testHelpers.waitForTimeStamp(loan2.maturity); + + const loansArray = await loanManager.getLoansForAddress(borrower, accountLoanCount - 2); const loanInfo = loanTestHelpers.parseLoansInfo(loansArray); - assert.equal(loanInfo.length, 1); // offset was from last loan added for account - - const lastLoan = loanInfo[0]; - - assert.equal(lastLoan.id.toNumber(), loan.id); - assert.equal(lastLoan.collateralAmount.toNumber(), loan.collateralAmount); - assert.equal(lastLoan.repaymentAmount.toNumber(), loan.repaymentAmount); - assert.equal("0x" + lastLoan.borrower.toString(16), loan.borrower); - assert.equal(lastLoan.productId.toNumber(), product.id); - assert.equal(lastLoan.state.toNumber(), loan.state); - assert.equal(lastLoan.maturity.toNumber(), loan.maturity); - assert.equal(lastLoan.disbursementTime.toNumber(), loan.maturity - product.term); - assert.equal(lastLoan.loanAmount.toNumber(), loan.loanAmount); - assert.equal(lastLoan.interestAmount.toNumber(), loan.interestAmount); + 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() { From 7aaea40d1c633828a05d76097fdbd336dae150d8 Mon Sep 17 00:00:00 2001 From: Peter Petrovics Date: Mon, 26 Feb 2018 15:31:03 +0000 Subject: [PATCH 19/20] added getLoanTuple common function --- contracts/LoanManager.sol | 40 +++++++++++++++------------------------ 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/contracts/LoanManager.sol b/contracts/LoanManager.sol index 61783bcf..ee71b5dc 100644 --- a/contracts/LoanManager.sol +++ b/contracts/LoanManager.sol @@ -208,19 +208,7 @@ contract LoanManager is Restricted { if (offset + i >= loans.length) { break; } - LoanData storage loan = loans[offset + i]; - 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; - - response[i] = [offset + i, loan.collateralAmount, loan.repaymentAmount, uint(loan.borrower), - loan.productId, uint(loanState), loan.maturity, disbursementTime, loanAmount, interestAmount]; + response[i] = getLoanTuple(offset + i); } } @@ -239,22 +227,24 @@ contract LoanManager is Restricted { if (offset + i >= loansForAddress.length) { break; } - uint loanId = loansForAddress[offset + i]; + response[i] = getLoanTuple(loansForAddress[offset + i]); + } + } - LoanData storage loan = loans[loanId]; - LoanProduct storage product = products[loan.productId]; + 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; + 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; + LoanState loanState = + loan.state == LoanState.Open && now >= loan.maturity ? LoanState.Defaulted : loan.state; - response[i] = [loanId, loan.collateralAmount, loan.repaymentAmount, uint(loan.borrower), - loan.productId, uint(loanState), loan.maturity, disbursementTime, loanAmount, interestAmount]; - } + 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 From 7352aeec7dac2568d8db465afcef70c13c98112a Mon Sep 17 00:00:00 2001 From: Peter Petrovics Date: Mon, 26 Feb 2018 17:13:04 +0000 Subject: [PATCH 20/20] gas price settings for testnetworks --- truffle.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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