diff --git a/contracts/rules/CreditRule.sol b/contracts/rules/CreditRule.sol new file mode 100644 index 0000000..6e94a83 --- /dev/null +++ b/contracts/rules/CreditRule.sol @@ -0,0 +1,192 @@ +pragma solidity ^0.5.0; + +// Copyright 2018 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import "../external/SafeMath.sol"; +import "../token/TransfersAgent.sol"; + + +/** + * Credit rules allows a budget holder to credit a user with an amount + * that the user can *only* spents in a token economy. A custom rule + * is deployed with a CreditRule acting as a TransfersAgent to execute + * transfers. CreditRule first transfers a minimum of credited amount and + * needed transfer amount, afterwards delegate the transfer execution to + * TransfersAgent itself. + * + * Steps to utilize CreditRule: + * - A custom rule is deployed by passing a CreditRule address to act as + * a transfers agent for the rule. + * - A token holder signs an executable transaction to execute the custom rule. + * - The budget holder signs an executable transaction to execute + * CreditRule::executeRule function with a credit amount, and the token + * holder's address and executeRule's function data as an argument. + */ +contract CreditRule is TransfersAgent { + + /* Usings */ + + using SafeMath for uint256; + + + /** Structs */ + + struct CreditInfo { + uint256 amount; + bool inProgress; + } + + + /* Storage */ + + address public budgetHolder; + + TransfersAgent public transfersAgent; + + mapping(address => CreditInfo) public credits; + + + /* Modifiers */ + + modifier onlyBudgetHolder() + { + require( + msg.sender == budgetHolder, + "Only budget holder is allowed to call." + ); + + _; + } + + + /* Special Functions */ + + constructor( + address _budgetHolder, + address _transfersAgent + ) + public + { + require( + _budgetHolder != address(0), + "Budget holder's address is null." + ); + + require( + _transfersAgent != address(0), + "Transfers agent's address is null." + ); + + budgetHolder = _budgetHolder; + + transfersAgent = TransfersAgent(_transfersAgent); + } + + + /* External Functions */ + + function executeRule( + uint256 _creditAmount, + address _to, // token holder address + bytes calldata _data // token holder execute rule data + ) + external + payable + onlyBudgetHolder + returns( + bool executionStatus_, + bytes memory returnData_ + ) + { + require( + _creditAmount != 0, + "Credit amount is 0." + ); + + require( + _to != address(0), + "To (token holder) address is null." + ); + + require( + credits[_to].inProgress == false, + "Re-entrancy occured in crediting process." + ); + + credits[_to].amount = _creditAmount; + credits[_to].inProgress = true; + + // solium-disable-next-line security/no-call-value + (executionStatus_, returnData_) = _to.call.value(msg.value)(_data); + } + + function executeTransfers( + address _from, + address[] calldata _transfersTo, + uint256[] calldata _transfersAmount + ) + external + { + if (credits[_from].inProgress) { + uint256 creditAmount = credits[_from].amount; + delete credits[_from]; + + uint256 sumAmount = 0; + + for(uint256 i = 0; i < _transfersAmount.length; ++i) { + sumAmount = sumAmount.add(_transfersAmount[i]); + } + + uint256 amountToTransferFromBudgetHolder = ( + sumAmount > creditAmount ? creditAmount : sumAmount + ); + + executeTransfer( + budgetHolder, + _from, + amountToTransferFromBudgetHolder + ); + } + + transfersAgent.executeTransfers( + _from, + _transfersTo, + _transfersAmount + ); + } + + + /* Private Functions */ + + function executeTransfer( + address _from, + address _beneficiary, + uint256 _amount + ) + private + { + address[] memory transfersTo = new address[](1); + transfersTo[0] = _beneficiary; + + uint256[] memory transfersAmount = new uint256[](1); + transfersAmount[0] = _amount; + + transfersAgent.executeTransfers( + _from, + transfersTo, + transfersAmount + ); + } +} diff --git a/contracts/test_doubles/unit_tests/TokenRulesSpy.sol b/contracts/test_doubles/unit_tests/TokenRulesSpy.sol index b3d6bca..f49c26e 100644 --- a/contracts/test_doubles/unit_tests/TokenRulesSpy.sol +++ b/contracts/test_doubles/unit_tests/TokenRulesSpy.sol @@ -14,19 +14,39 @@ pragma solidity ^0.5.0; // See the License for the specific language governing permissions and // limitations under the License. +import "../../token/EIP20TokenInterface.sol"; + contract TokenRulesSpy { + /* Structs */ + + struct TransactionEntry { + address from; + address[] transfersTo; + uint256[] transfersAmount; + } + + /* Storage */ + EIP20TokenInterface public token; + + TransactionEntry[] public transactions; + + uint256 public transactionsLength; + mapping (address => bool) public allowedTransfers; - address public recordedFrom; - address[] public recordedTransfersTo; - uint256 public recordedTransfersToLength; + /* Special Functions */ + + constructor(EIP20TokenInterface _token) + public + { + require(address(_token) != address(0), "Token address is null."); - uint256[] public recordedTransfersAmount; - uint256 public recordedTransfersAmountLength; + token = _token; + } /* External Functions */ @@ -43,6 +63,30 @@ contract TokenRulesSpy { allowedTransfers[msg.sender] = false; } + function fromTransaction(uint256 index) + external + view + returns (address) + { + return transactions[index].from; + } + + function transfersToTransaction(uint256 index) + external + view + returns (address[] memory) + { + return transactions[index].transfersTo; + } + + function transfersAmountTransaction(uint256 index) + external + view + returns (uint256[] memory) + { + return transactions[index].transfersAmount; + } + function executeTransfers( address _from, address[] calldata _transfersTo, @@ -50,13 +94,25 @@ contract TokenRulesSpy { ) external { - recordedFrom = _from; + TransactionEntry memory entry = TransactionEntry({ + from: _from, + transfersTo: new address[](0), + transfersAmount: new uint256[](0) + }); + + transactions.push(entry); + + for (uint256 i = 0; i < _transfersTo.length; ++i) { + transactions[transactionsLength].transfersTo.push(_transfersTo[i]); + } - recordedTransfersTo = _transfersTo; - recordedTransfersToLength = _transfersTo.length; + for (uint256 i = 0; i < _transfersAmount.length; ++i) { + transactions[transactionsLength].transfersAmount.push( + _transfersAmount[i] + ); + } - recordedTransfersAmount = _transfersAmount; - recordedTransfersAmountLength = _transfersAmount.length; + ++transactionsLength; } } diff --git a/contracts/test_doubles/unit_tests/credit_rule/CustomRuleWithCredit.sol b/contracts/test_doubles/unit_tests/credit_rule/CustomRuleWithCredit.sol new file mode 100644 index 0000000..b116596 --- /dev/null +++ b/contracts/test_doubles/unit_tests/credit_rule/CustomRuleWithCredit.sol @@ -0,0 +1,87 @@ +pragma solidity ^0.5.0; + +// Copyright 2018 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import "../../../external/SafeMath.sol"; +import "../../../rules/CreditRule.sol"; + +contract CustomRuleWithCredit { + + /* Usings */ + + using SafeMath for uint256; + + event Pay( + address _to, + uint256 _amount + ); + + /* Storage */ + + CreditRule public creditRule; + + bool public markedToFail; + + + /* Special Functions */ + + constructor( + address _creditRule + ) + public + { + require( + address(_creditRule) != address(0), + "Credit rule's address is null." + ); + + creditRule = CreditRule(_creditRule); + } + + + /* External Functions */ + + function makeMeFail() + external + { + markedToFail = true; + } + + function pay( + address _to, + uint256 _amount + ) + external + { + require( + !markedToFail, + "The function is marked to fail." + ); + + address[] memory transfersTo = new address[](1); + transfersTo[0] = _to; + + uint256[] memory transfersAmount = new uint256[](1); + transfersAmount[0] = _amount; + + creditRule.executeTransfers( + msg.sender, + transfersTo, + transfersAmount + ); + + emit Pay(_to, _amount); + } +} diff --git a/contracts/token/TokenRules.sol b/contracts/token/TokenRules.sol index 279d2df..59dd1f5 100644 --- a/contracts/token/TokenRules.sol +++ b/contracts/token/TokenRules.sol @@ -14,9 +14,11 @@ pragma solidity ^0.5.0; // See the License for the specific language governing permissions and // limitations under the License. -import "./EIP20TokenInterface.sol"; import "../organization/Organized.sol"; +import "./EIP20TokenInterface.sol"; +import "./TransfersAgent.sol"; + /** * @notice Register of whitelisted rules that are allowed to initiate transfers * from a token holder accounts. @@ -33,7 +35,7 @@ import "../organization/Organized.sol"; * During a execution, rule can call TokenRules.executeTransfers() * function only once. */ -contract TokenRules is Organized { +contract TokenRules is Organized, TransfersAgent { /* Events */ diff --git a/contracts/token/TransfersAgent.sol b/contracts/token/TransfersAgent.sol new file mode 100644 index 0000000..80e66eb --- /dev/null +++ b/contracts/token/TransfersAgent.sol @@ -0,0 +1,34 @@ +pragma solidity ^0.5.0; + +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +interface TransfersAgent { + + /** + * @dev Transfers from the specified account to all beneficiary + * accounts corresponding amounts. + * + * @param _from An address from which transfer is done. + * @param _transfersTo List of addresses to transfer. + * @param _transfersAmount List of amounts to transfer. + */ + function executeTransfers( + address _from, + address[] calldata _transfersTo, + uint256[] calldata _transfersAmount + ) + external; +} diff --git a/package-lock.json b/package-lock.json index b061eb1..8207da7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2461,7 +2461,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -2482,12 +2483,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2502,17 +2505,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -2629,7 +2635,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -2641,6 +2648,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -2655,6 +2663,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -2662,12 +2671,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -2686,6 +2697,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -2766,7 +2778,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -2778,6 +2791,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -2863,7 +2877,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -2899,6 +2914,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -2918,6 +2934,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -2961,12 +2978,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, @@ -11529,7 +11548,7 @@ "eventemitter3": "3.1.0", "lodash": "^4.17.11", "url-parse": "1.4.4", - "websocket": "git://github.com/frozeman/WebSocket-Node.git#6c72925e3f8aaaea8dc8450f97627e85263999f2", + "websocket": "git://github.com/frozeman/WebSocket-Node.git#browserifyCompatible", "xhr2-cookies": "1.1.0" }, "dependencies": { diff --git a/test/credit_rule/constructor.js b/test/credit_rule/constructor.js new file mode 100644 index 0000000..b6dad7d --- /dev/null +++ b/test/credit_rule/constructor.js @@ -0,0 +1,69 @@ +// Copyright 2018 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const Utils = require('../test_lib/utils'); +const { AccountProvider } = require('../test_lib/utils'); + +const CreditRule = artifacts.require('CreditRule'); + +contract('CreditRule::constructor', async () => { + contract('Negative Tests', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Reverts if the credit budget holder\'s address is null.', async () => { + await Utils.expectRevert( + CreditRule.new( + Utils.NULL_ADDRESS, // credit budget holder's address + accountProvider.get(), // token rules's address + ), + 'Should revert as the credit budget holder\'s address is null.', + 'Budget holder\'s address is null.', + ); + }); + + it('Reverts if the transfers agent\'s address is null.', async () => { + await Utils.expectRevert( + CreditRule.new( + accountProvider.get(), // credit budget holder's address + Utils.NULL_ADDRESS, // transfers agent's address + ), + 'Should revert as the transfers agent\'s address is null.', + 'Transfers agent\'s address is null.', + ); + }); + }); + + contract('Storage', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + it('Checks that passed arguments are set correctly.', async () => { + const creditBudgetHolder = accountProvider.get(); + const transfersAgent = accountProvider.get(); + + const creditRule = await CreditRule.new( + creditBudgetHolder, + transfersAgent, + ); + + assert.strictEqual( + (await creditRule.budgetHolder.call()), + creditBudgetHolder, + ); + + assert.strictEqual( + (await creditRule.transfersAgent.call()), + transfersAgent, + ); + }); + }); +}); diff --git a/test/credit_rule/execute_transfers.js b/test/credit_rule/execute_transfers.js new file mode 100644 index 0000000..7aaeca4 --- /dev/null +++ b/test/credit_rule/execute_transfers.js @@ -0,0 +1,281 @@ +// Copyright 2018 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const Utils = require('../test_lib/utils.js'); +const { AccountProvider } = require('../test_lib/utils'); +const { TokenHolderUtils } = require('../token_holder/utils.js'); + +const TokenHolder = artifacts.require('TokenHolder'); +const CreditRule = artifacts.require('CreditRule'); +const CustomRuleWithCredit = artifacts.require('CustomRuleWithCredit'); + +const budgetHolderSessionPublicKey = '0xBbfd1BF77dA692abc82357aC001415b98d123d17'; +const budgetHolderSessionPrivateKey = '0x6817f551bbc3e12b8fe36787ab192c921390d6176a3324ed02f96935a370bc41'; +const tokenHolderSessionPublicKey = '0x62502C4DF73935D0D10054b0Fb8cC036534C6fb0'; +const tokenHolderSessionPrivateKey = '0xa8225c01ceeaf01d7bc7c1b1b929037bd4050967c5730c0b854263121b8399f3'; + +async function prepare( + accountProvider, + budgetHolderInitialBalance, + tokenHolderInitialBalance, +) { + const { + utilityToken: token, + } = await TokenHolderUtils.createUtilityMockToken(); + + const { + tokenRules, + } = await TokenHolderUtils.createMockTokenRules(token.address); + + const { + tokenHolder: budgetHolder, + tokenHolderOwnerAddress: budgetHolderOwnerAddress, + } = await TokenHolderUtils.createTokenHolder( + accountProvider, token, tokenRules, + ); + + await token.increaseBalance(budgetHolder.address, budgetHolderInitialBalance); + + const budgetHolderSessionKeySpendingLimit = 33; + const budgetHolderSessionKeyExpirationDelta = 300; + + await TokenHolderUtils.authorizeSessionKey( + budgetHolder, budgetHolderOwnerAddress, + budgetHolderSessionPublicKey, + budgetHolderSessionKeySpendingLimit, + budgetHolderSessionKeyExpirationDelta, + ); + + const { + tokenHolder, + tokenHolderOwnerAddress, + } = await TokenHolderUtils.createTokenHolder( + accountProvider, token, tokenRules, + ); + + await token.increaseBalance(tokenHolder.address, tokenHolderInitialBalance); + + const tokenHolderSessionKeySpendingLimit = 22; + const tokenHolderSessionKeyExpirationDelta = 200; + + await TokenHolderUtils.authorizeSessionKey( + tokenHolder, tokenHolderOwnerAddress, + tokenHolderSessionPublicKey, + tokenHolderSessionKeySpendingLimit, + tokenHolderSessionKeyExpirationDelta, + ); + + const creditRule = await CreditRule.new( + budgetHolder.address, tokenRules.address, + ); + + const customRule = await CustomRuleWithCredit.new(creditRule.address); + + return { + token, + tokenRules, + budgetHolderOwnerAddress, + budgetHolder, + budgetHolderSessionKeySpendingLimit, + budgetHolderSessionKeyExpirationDelta, + tokenHolderOwnerAddress, + tokenHolder, + tokenHolderSessionKeySpendingLimit, + tokenHolderSessionKeyExpirationDelta, + creditRule, + customRule, + }; +} + +async function tokenHolderExecuteRuleCallPrefix() { + const tokenHolder = await TokenHolder.new(); + return tokenHolder.EXECUTE_RULE_CALLPREFIX.call(); +} + +async function checkTransactions( + tokenRulesSpy, + creditBudgetHolderAddr, + creditAmount, + userTokenHolderAddr, + beneficiaries, + amounts, +) { + const transactionsLength = await tokenRulesSpy.transactionsLength.call(); + + // Transactions count registered in TokenRulesSpy should be 2: + // - transfers from CreditBudgetHolder to UserTokenHolder + // - transfers from UserTokenHolder instance to beneficiaries + assert.isOk( + transactionsLength.eqn(2), + ); + + // First transfer (crediting) is from CreditBudgetHolder to UserTokenHolder. + assert.strictEqual( + await tokenRulesSpy.fromTransaction.call(0), + creditBudgetHolderAddr, + ); + + // UserTokenHolder address is the only beneficiary for the first transfer. + const firstTransfersToTransaction = await tokenRulesSpy.transfersToTransaction.call(0); + assert.strictEqual( + firstTransfersToTransaction.length, + 1, + ); + assert.strictEqual( + firstTransfersToTransaction[0], + userTokenHolderAddr, + ); + + // UserTokenHolder credited amount is calculated, by: + // min(sum(amounts), creditAmount) + const firstTransfersAmountTransaction = await tokenRulesSpy.transfersAmountTransaction.call(0); + assert.strictEqual( + firstTransfersAmountTransaction.length, + 1, + ); + assert.isOk( + (firstTransfersAmountTransaction[0]).eqn( + Math.min( + // Calculates sum of the amounts elements. + amounts.reduce((accumulator, currentValue) => accumulator + currentValue), + creditAmount, + ), + ), + ); + + // Second transfers is from UserTokenHolder to the beneficiaries. + assert.strictEqual( + await tokenRulesSpy.fromTransaction.call(1), + userTokenHolderAddr, + ); + + const secondTransfersToTransaction = await tokenRulesSpy.transfersToTransaction.call(1); + assert.strictEqual( + secondTransfersToTransaction.length, + beneficiaries.length, + ); + + for (let i = 0; i < secondTransfersToTransaction.length; i += 1) { + assert.strictEqual( + secondTransfersToTransaction[i], + beneficiaries[i], + ); + } + + const secondTransfersAmountTransaction = await tokenRulesSpy.transfersAmountTransaction.call(1); + assert.strictEqual( + secondTransfersAmountTransaction.length, + amounts.length, + ); + + for (let i = 0; i < secondTransfersAmountTransaction.length; i += 1) { + assert.isOk( + (secondTransfersAmountTransaction[i]).eqn( + amounts[i], + ), + ); + } +} + +contract('Credit::execute_transfers', async () => { + contract('Happy Path', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Checks that passed arguments are set correctly.', async () => { + const { + tokenRules, + budgetHolder, + tokenHolder, + creditRule, + customRule, + } = await prepare( + accountProvider, + 222, // budgetHolderInitialBalance + 111, // tokenHolderInitialBalance + ); + + const beneficiaryAddress = accountProvider.get(); + const amount = 11; + + const tokenHolderSessionKeyData = await tokenHolder.sessionKeys.call( + tokenHolderSessionPublicKey, + ); + + const customRuleWithCreditPayFunctionData = customRule.contract.methods.pay( + beneficiaryAddress, amount, + ).encodeABI(); + + const { + exTxSignature: customRuleExTxSignature, + } = await Utils.generateExTx( + tokenHolder.address, + customRule.address, + customRuleWithCreditPayFunctionData, + tokenHolderSessionKeyData.nonce.toNumber(), + await tokenHolderExecuteRuleCallPrefix(), + tokenHolderSessionPrivateKey, + ); + + const tokenHolderExecuteRuleFunctionData = tokenHolder.contract.methods.executeRule( + customRule.address, + customRuleWithCreditPayFunctionData, + tokenHolderSessionKeyData.nonce.toNumber(), + customRuleExTxSignature.r, + customRuleExTxSignature.s, + customRuleExTxSignature.v, + ).encodeABI(); + + const creditAmount = 5; + + const creditRuleExecuteRuleFunctionData = creditRule.contract.methods.executeRule( + creditAmount, + tokenHolder.address, + tokenHolderExecuteRuleFunctionData, + ).encodeABI(); + + const budgetHolderSessionKeyData = await budgetHolder.sessionKeys.call( + budgetHolderSessionPublicKey, + ); + + const { + exTxSignature: tokenHolderExecuteRuleExTxSignature, + } = await Utils.generateExTx( + budgetHolder.address, + creditRule.address, + creditRuleExecuteRuleFunctionData, + budgetHolderSessionKeyData.nonce.toNumber(), + await tokenHolderExecuteRuleCallPrefix(), + budgetHolderSessionPrivateKey, + ); + + await budgetHolder.executeRule( + creditRule.address, + creditRuleExecuteRuleFunctionData, + budgetHolderSessionKeyData.nonce.toNumber(), + tokenHolderExecuteRuleExTxSignature.r, + tokenHolderExecuteRuleExTxSignature.s, + tokenHolderExecuteRuleExTxSignature.v, + ); + + await checkTransactions( + tokenRules, + budgetHolder.address, + creditAmount, + tokenHolder.address, + [beneficiaryAddress], + [amount], + ); + }); + }); +}); diff --git a/test/pricer_rule/pay.js b/test/pricer_rule/pay.js index bcd4038..6323af8 100644 --- a/test/pricer_rule/pay.js +++ b/test/pricer_rule/pay.js @@ -297,31 +297,35 @@ contract('PricerRule::pay', async () => { new BN(conversionRate), new BN(conversionRateDecimals), ); - const actualFromAddress = await tokenRules.recordedFrom.call(); - const actualToAddress1 = await tokenRules.recordedTransfersTo.call(0); - const actualToAddress2 = await tokenRules.recordedTransfersTo.call(1); - const actualTransfersToLength = await tokenRules.recordedTransfersToLength.call(); + const recordedTxLength = await tokenRules.transactionsLength.call(); + assert.isOk(recordedTxLength.eqn(1)); + + const actualFromAddress = await tokenRules.fromTransaction.call(0); + + const transfersToTransaction = await tokenRules.transfersToTransaction.call(0); + assert.strictEqual(transfersToTransaction.length, 2); + + const actualToAddress1 = transfersToTransaction[0]; + const actualToAddress2 = transfersToTransaction[1]; const tenPowerTokenDecimals = (new BN(10)).pow(new BN(tokenDecimals)); // 1000 BTs = 1000*10^18 BTWei const expectedTransferAmount1 = new BN(1000).mul(tenPowerTokenDecimals); // 500 BTs = 500*10^18 BTWei const expectedTransferAmount2 = new BN(500).mul(tenPowerTokenDecimals); - const transferredAmount1 = await tokenRules.recordedTransfersAmount.call(0); - const transferredAmount2 = await tokenRules.recordedTransfersAmount.call(1); - const actualTransfersAmountLength = await tokenRules.recordedTransfersAmountLength.call(); + const transfersAmountTransaction = await tokenRules.transfersAmountTransaction.call(0); + assert.strictEqual(transfersAmountTransaction.length, 2); + + const transferredAmount1 = transfersAmountTransaction[0]; + const transferredAmount2 = transfersAmountTransaction[1]; assert.strictEqual( actualFromAddress, fromAddress, ); - assert.isOk( - actualTransfersToLength.eqn(2), - ); - assert.strictEqual( actualToAddress1, to1, @@ -332,10 +336,6 @@ contract('PricerRule::pay', async () => { to2, ); - assert.isOk( - actualTransfersAmountLength.eqn(2), - ); - assert.isOk( transferredAmount1.eq(expectedTransferAmount1), ); @@ -420,11 +420,17 @@ contract('PricerRule::pay', async () => { new BN(conversionRate), new BN(conversionRateDecimals), ); - const actualFromAddress = await tokenRules.recordedFrom.call(); - const actualToAddress1 = await tokenRules.recordedTransfersTo.call(0); - const actualToAddress2 = await tokenRules.recordedTransfersTo.call(1); - const actualTransfersToLength = await tokenRules.recordedTransfersToLength.call(); + const recordedTxLength = await tokenRules.transactionsLength.call(); + assert.isOk(recordedTxLength.eqn(1)); + + const actualFromAddress = await tokenRules.fromTransaction.call(0); + + const transfersToTransaction = await tokenRules.transfersToTransaction.call(0); + assert.strictEqual(transfersToTransaction.length, 2); + + const actualToAddress1 = transfersToTransaction[0]; + const actualToAddress2 = transfersToTransaction[1]; // Number of bt needs to be transferred for a payment shouldn’t depend on // requiredPriceOracleDecimals. @@ -435,20 +441,18 @@ contract('PricerRule::pay', async () => { const expectedTransferAmount1 = new BN(1000).mul(tenPowerTokenDecimals); // 500 BTs = 500*10^18 BTWei const expectedTransferAmount2 = new BN(500).mul(tenPowerTokenDecimals); - const transferredAmount1 = await tokenRules.recordedTransfersAmount.call(0); - const transferredAmount2 = await tokenRules.recordedTransfersAmount.call(1); - const actualTransfersAmountLength = await tokenRules.recordedTransfersAmountLength.call(); + const transfersAmountTransaction = await tokenRules.transfersAmountTransaction.call(0); + assert.strictEqual(transfersAmountTransaction.length, 2); + + const transferredAmount1 = transfersAmountTransaction[0]; + const transferredAmount2 = transfersAmountTransaction[1]; assert.strictEqual( actualFromAddress, fromAddress, ); - assert.isOk( - actualTransfersToLength.eqn(2), - ); - assert.strictEqual( actualToAddress1, to1, @@ -459,10 +463,6 @@ contract('PricerRule::pay', async () => { to2, ); - assert.isOk( - actualTransfersAmountLength.eqn(2), - ); - assert.isOk( expectedTransferAmount1.eq(convertedAmount1BN), ); @@ -547,31 +547,35 @@ contract('PricerRule::pay', async () => { new BN(conversionRate), new BN(conversionRateDecimals), ); - const actualFromAddress = await tokenRules.recordedFrom.call(); - const actualToAddress1 = await tokenRules.recordedTransfersTo.call(0); - const actualToAddress2 = await tokenRules.recordedTransfersTo.call(1); - const actualTransfersToLength = await tokenRules.recordedTransfersToLength.call(); + const recordedTxLength = await tokenRules.transactionsLength.call(); + assert.isOk(recordedTxLength.eqn(1)); + + const actualFromAddress = await tokenRules.fromTransaction.call(0); + + const transfersToTransaction = await tokenRules.transfersToTransaction.call(0); + assert.strictEqual(transfersToTransaction.length, 2); + + const actualToAddress1 = transfersToTransaction[0]; + const actualToAddress2 = transfersToTransaction[1]; const tenPowerEIP20TokenDecimal = (new BN(10)).pow(new BN(tokenDecimals)); // 1000 BTs = 1000*10^5 BTWei const expectedTransferAmount1 = new BN(1000).mul(tenPowerEIP20TokenDecimal); // 500 BTs = 500*10^5 BTWei const expectedTransferAmount2 = new BN(500).mul(tenPowerEIP20TokenDecimal); - const transferredAmount1 = await tokenRules.recordedTransfersAmount.call(0); - const transferredAmount2 = await tokenRules.recordedTransfersAmount.call(1); - const actualTransfersAmountLength = await tokenRules.recordedTransfersAmountLength.call(); + const transfersAmountTransaction = await tokenRules.transfersAmountTransaction.call(0); + assert.strictEqual(transfersAmountTransaction.length, 2); + + const transferredAmount1 = transfersAmountTransaction[0]; + const transferredAmount2 = transfersAmountTransaction[1]; assert.strictEqual( actualFromAddress, fromAddress, ); - assert.isOk( - actualTransfersToLength.eqn(2), - ); - assert.strictEqual( actualToAddress1, to1, @@ -582,10 +586,6 @@ contract('PricerRule::pay', async () => { to2, ); - assert.isOk( - actualTransfersAmountLength.eqn(2), - ); - assert.isOk( transferredAmount1.eq(expectedTransferAmount1), ); diff --git a/test/pricer_rule/utils.js b/test/pricer_rule/utils.js index 984894c..58a0677 100644 --- a/test/pricer_rule/utils.js +++ b/test/pricer_rule/utils.js @@ -78,7 +78,7 @@ module.exports.createTokenEconomy = async (accountProvider, config = {}, eip20To const tokenDecimals = eip20TokenConfig.decimals; const token = await this.createEIP20Token(eip20TokenConfig); - const tokenRules = await TokenRulesSpy.new(); + const tokenRules = await TokenRulesSpy.new(token.address); const baseCurrencyCode = 'OST'; diff --git a/test/token_holder/authorize_session.js b/test/token_holder/authorize_session.js index a3c8a21..de5260f 100644 --- a/test/token_holder/authorize_session.js +++ b/test/token_holder/authorize_session.js @@ -29,8 +29,9 @@ async function prepare( sessionPublicKeyToAuthorize, ) { const { utilityToken } = await TokenHolderUtils.createUtilityMockToken(); - - const { tokenRules } = await TokenHolderUtils.createMockTokenRules(); + const { tokenRules } = await TokenHolderUtils.createMockTokenRules( + utilityToken.address, + ); const { tokenHolderOwnerAddress, diff --git a/test/token_holder/execute_redemption.js b/test/token_holder/execute_redemption.js index abe9a17..cdd1f12 100644 --- a/test/token_holder/execute_redemption.js +++ b/test/token_holder/execute_redemption.js @@ -102,7 +102,9 @@ async function prepare( ) { const { utilityToken } = await TokenHolderUtils.createUtilityMockToken(); - const { tokenRules } = await TokenHolderUtils.createMockTokenRules(); + const { tokenRules } = await TokenHolderUtils.createMockTokenRules( + utilityToken.address, + ); const { tokenHolderOwnerAddress, diff --git a/test/token_holder/execute_rule.js b/test/token_holder/execute_rule.js index b8a75c0..58b32b5 100644 --- a/test/token_holder/execute_rule.js +++ b/test/token_holder/execute_rule.js @@ -282,7 +282,9 @@ async function prepare( ) { const { utilityToken } = await TokenHolderUtils.createUtilityMockToken(); - const { tokenRules } = await TokenHolderUtils.createMockTokenRules(); + const { tokenRules } = await TokenHolderUtils.createMockTokenRules( + utilityToken.address, + ); const { tokenHolderOwnerAddress, diff --git a/test/token_holder/logout.js b/test/token_holder/logout.js index 0a89ff1..6d5da74 100644 --- a/test/token_holder/logout.js +++ b/test/token_holder/logout.js @@ -26,7 +26,9 @@ async function prepare( ) { const { utilityToken } = await TokenHolderUtils.createUtilityMockToken(); - const { tokenRules } = await TokenHolderUtils.createMockTokenRules(); + const { tokenRules } = await TokenHolderUtils.createMockTokenRules( + utilityToken.address, + ); const authorizedSessionPublicKey = accountProvider.get(); diff --git a/test/token_holder/revoke_session.js b/test/token_holder/revoke_session.js index c92ad81..c8c8045 100644 --- a/test/token_holder/revoke_session.js +++ b/test/token_holder/revoke_session.js @@ -28,7 +28,9 @@ async function prepare( ) { const { utilityToken } = await TokenHolderUtils.createUtilityMockToken(); - const { tokenRules } = await TokenHolderUtils.createMockTokenRules(); + const { tokenRules } = await TokenHolderUtils.createMockTokenRules( + utilityToken.address, + ); const { tokenHolderOwnerAddress, diff --git a/test/token_holder/utils.js b/test/token_holder/utils.js index 40ef1e1..2baf03b 100644 --- a/test/token_holder/utils.js +++ b/test/token_holder/utils.js @@ -25,12 +25,11 @@ class TokenHolderUtils { const utilityToken = await UtilityTokenFake.new( 'OST', 'Open Simple Token', 1, ); - return { utilityToken }; } - static async createMockTokenRules() { - const tokenRules = await TokenRulesSpy.new(); + static async createMockTokenRules(tokenAddress) { + const tokenRules = await TokenRulesSpy.new(tokenAddress); return { tokenRules }; } diff --git a/tools/docker_run_slither.sh b/tools/docker_run_slither.sh index b82a8fe..54c8c4b 100755 --- a/tools/docker_run_slither.sh +++ b/tools/docker_run_slither.sh @@ -11,8 +11,10 @@ docker exec -it "${container}" bash -c \ " cd /share \ && solc-select 0.5.7 \ && slither . \ + --truffle-ignore-compile \ --config-file slither.conf.json \ " + slither_result=$? docker kill "${container}" || exit 1