├── .gitattributes ├── .gitignore ├── .solcover.js ├── .soliumignore ├── .soliumrc.json ├── .travis.yml ├── LICENSE ├── README.md ├── contracts ├── Migrations.sol ├── escrow │ ├── Escrow.sol │ ├── EscrowProxy.sol │ ├── EscrowSpec.md │ └── IEscrow.sol ├── powerUps │ ├── PowerUps.sol │ └── PowerUpsSpec.md ├── registry │ ├── ContractManager.sol │ └── ContractManagerSpec.md ├── rewards │ ├── OBRewards.sol │ └── RewardsSpec.md ├── test │ └── TestToken.sol └── token │ ├── ITokenContract.sol │ └── OBToken.sol ├── migrations ├── 1_initial_migration.js ├── escrow │ ├── 2_Escrow_migration.js │ └── 7_EscrowProxy_migration.js ├── powerUps │ └── 6_Powered_Ups_migration.js ├── registry │ └── 3_contract_manager_migration.js ├── rewards │ └── 5_Rewards_Migration.js └── token │ └── 4_OB_Token_migration.js ├── package-lock.json ├── package.json ├── scripts ├── coverage.sh ├── signing.js ├── test.sh └── usage.txt ├── test ├── escrow │ └── 1_Escrow_test.js ├── helper.js ├── powerUps │ └── 3_PowerUps_tests.js ├── registry │ └── 2_contract_manager_test.js └── rewards │ └── 6_OB_Rewards_Test.js └── truffle.js /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | coverage.json 4 | build/ 5 | -------------------------------------------------------------------------------- /.solcover.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | norpc: true, 3 | copyPackages:["openzeppelin-solidity"], 4 | port: 8555, 5 | testCommand: 'node --max-old-space-size=4096 ../node_modules/.bin/truffle test --network coverage', 6 | compileCommand: 'node --max-old-space-size=4096 ../node_modules/.bin/truffle compile --network coverage', 7 | skipFiles : ["escrow/EscrowProxy.sol", "test/TestToken.sol", "token/ITokenContract.sol"] 8 | }; -------------------------------------------------------------------------------- /.soliumignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | contracts/test 3 | Migration.sol 4 | -------------------------------------------------------------------------------- /.soliumrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solium:all", 3 | "plugins": ["security"], 4 | "rules": { 5 | "error-reason": "off", 6 | "indentation": ["error", 4], 7 | "lbrace": "off", 8 | "linebreak-style": ["error", "unix"], 9 | "no-constant": ["error"], 10 | "no-empty-blocks": "off", 11 | "quotes": ["error", "double"], 12 | "visibility-first": "error", 13 | "max-len": ["error", 79], 14 | "security/enforce-explicit-visibility": ["error"], 15 | "security/no-block-members": ["warning"], 16 | "security/no-inline-assembly": ["warning"] 17 | } 18 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | sudo: false 3 | group: beta 4 | language: node_js 5 | node_js: 6 | - "8" 7 | cache: 8 | directories: 9 | - node_modules 10 | matrix: 11 | fast_finish: true 12 | allow_failures: 13 | - env: SOLIDITY_COVERAGE=true 14 | install: 15 | - npm install -g truffle 16 | - npm install 17 | before_script: 18 | - truffle version 19 | script: 20 | - npm run lint:sol 21 | - npm run coverage -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 OpenBazaar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenBazaar-SmartContracts 2 | [![Build Status](https://travis-ci.org/OpenBazaar/smart-contracts.svg?branch=master)](https://travis-ci.org/OpenBazaar/smart-contracts) 3 | Coverage Status 4 | 5 | This repository contains all OpenBazaar smart contracts 6 | ## Getting Started 7 | 8 | It integrates with [Truffle](https://github.com/ConsenSys/truffle), an Ethereum development environment. Please install Truffle. 9 | 10 | ```sh 11 | npm install -g truffle 12 | 13 | ``` 14 | Clone OpenBazaar-SmartContracts 15 | 16 | ```sh 17 | git clone https://github.com/OpenBazaar/smart-contracts.git 18 | cd smart-contracts 19 | npm i 20 | ``` 21 | 22 | Compile and Deploy 23 | ------------------ 24 | These commands apply to the RPC provider running on port 8545. You may want to have TestRPC running in the background. They are really wrappers around the [corresponding Truffle commands](http://truffleframework.com/docs/advanced/commands). 25 | 26 | ### Compile all contracts to obtain ABI and bytecode: 27 | 28 | ```bash 29 | npm run compile 30 | ``` 31 | 32 | ### Migrate all contracts required for the basic framework onto network associated with RPC provider: 33 | 34 | ```bash 35 | npm run migrate 36 | ``` 37 | Network Artifacts 38 | ----------------- 39 | 40 | ### Show the deployed addresses of all contracts on all networks: 41 | 42 | ```bash 43 | npm run networks 44 | ``` 45 | 46 | Testing 47 | ------------------- 48 | ### Run all tests (requires Node version >=8 for `async/await`, and will automatically run TestRPC in the background): 49 | 50 | ```bash 51 | npm test 52 | ``` 53 | 54 | Test Coverage 55 | ------------------- 56 | ### Get test coverage stats(requires Node version >=8 for `async/await`, and will automatically run TestRPC in the background): 57 | 58 | ```bash 59 | npm run coverage 60 | ``` 61 | 62 | License 63 | ------------------- 64 | Openbazaar smart contracts are released under the [MIT License](LICENSE). 65 | -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.5.7; 2 | 3 | 4 | contract Migrations { 5 | address public owner; 6 | uint public lastCompletedMigration; 7 | 8 | constructor() public { 9 | owner = msg.sender; 10 | } 11 | 12 | modifier restricted() { 13 | if (msg.sender == owner) _; 14 | } 15 | 16 | function setCompleted(uint completed) public restricted { 17 | lastCompletedMigration = completed; 18 | } 19 | 20 | function upgrade(address newAddress) public restricted { 21 | Migrations upgraded = Migrations(newAddress); 22 | upgraded.setCompleted(lastCompletedMigration); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /contracts/escrow/Escrow.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.5.7; 2 | 3 | import "openzeppelin-solidity/contracts/math/SafeMath.sol"; 4 | import "../token/ITokenContract.sol"; 5 | 6 | 7 | /** 8 | * @title OpenBazaar Escrow 9 | * @author OB1 10 | * @notice Holds ETH and ERC20 tokens for moderated trades on the OpenBazaar 11 | * platform. See the specification here: 12 | * https://github.com/OpenBazaar/smart-contracts/blob/master/contracts/escrow/EscrowSpec.md 13 | * @dev Do not use this contract with tokens that do not strictly adhere to the 14 | * ERC20 token standard. In particular, all successful calls to `transfer` and 15 | * `transferFrom` on the token contract MUST return true. Non-compliant tokens 16 | * may get trapped in this contract forever. See the specification for more 17 | * details. 18 | */ 19 | contract Escrow { 20 | 21 | using SafeMath for uint256; 22 | 23 | enum Status {FUNDED, RELEASED} 24 | 25 | enum TransactionType {ETHER, TOKEN} 26 | 27 | event Executed( 28 | bytes32 indexed scriptHash, 29 | address payable[] destinations, 30 | uint256[] amounts 31 | ); 32 | 33 | event FundAdded( 34 | bytes32 indexed scriptHash, 35 | address indexed from, 36 | uint256 valueAdded 37 | ); 38 | 39 | event Funded( 40 | bytes32 indexed scriptHash, 41 | address indexed from, 42 | uint256 value 43 | ); 44 | 45 | struct Transaction { 46 | uint256 value; 47 | uint256 lastModified; 48 | Status status; 49 | TransactionType transactionType; 50 | uint8 threshold; 51 | uint32 timeoutHours; 52 | address buyer; 53 | address seller; 54 | address tokenAddress; //address of ERC20 token if applicable 55 | address moderator; 56 | uint256 released; 57 | uint256 noOfReleases; //number of times funds have been released 58 | mapping(address => bool) isOwner; 59 | //tracks who has authorized release of funds from escrow 60 | mapping(bytes32 => bool) voted; 61 | //tracks who has received funds released from escrow 62 | mapping(address => bool) beneficiaries; 63 | } 64 | 65 | mapping(bytes32 => Transaction) public transactions; 66 | 67 | uint256 public transactionCount = 0; 68 | 69 | //maps address to array of scriptHashes of all OpenBazaar transacations for 70 | //which they are either the buyer or the seller 71 | mapping(address => bytes32[]) private partyVsTransaction; 72 | 73 | modifier transactionExists(bytes32 scriptHash) { 74 | require( 75 | transactions[scriptHash].value != 0, "Transaction does not exist" 76 | ); 77 | _; 78 | } 79 | 80 | modifier transactionDoesNotExist(bytes32 scriptHash) { 81 | require(transactions[scriptHash].value == 0, "Transaction exists"); 82 | _; 83 | } 84 | 85 | modifier inFundedState(bytes32 scriptHash) { 86 | require( 87 | transactions[scriptHash].status == Status.FUNDED, 88 | "Transaction is not in FUNDED state" 89 | ); 90 | _; 91 | } 92 | 93 | modifier fundsExist(bytes32 scriptHash) { 94 | require( 95 | transactions[scriptHash].value.sub(transactions[scriptHash].released) > 0, 96 | "All funds has been released" 97 | ); 98 | _; 99 | } 100 | 101 | modifier nonZeroAddress(address addressToCheck) { 102 | require(addressToCheck != address(0), "Zero address passed"); 103 | _; 104 | } 105 | 106 | modifier checkTransactionType( 107 | bytes32 scriptHash, 108 | TransactionType transactionType 109 | ) 110 | { 111 | require( 112 | transactions[scriptHash].transactionType == transactionType, 113 | "Transaction type does not match" 114 | ); 115 | _; 116 | } 117 | 118 | modifier onlyBuyer(bytes32 scriptHash) { 119 | require( 120 | msg.sender == transactions[scriptHash].buyer, 121 | "The initiator of the transaction is not buyer" 122 | ); 123 | _; 124 | } 125 | 126 | /** 127 | * @notice Registers a new OpenBazaar transaction to the contract 128 | * @dev To be used for moderated ETH transactions 129 | * @param buyer The buyer associated with the OpenBazaar transaction 130 | * @param seller The seller associated with the OpenBazaar transaction 131 | * @param moderator The moderator (if any) associated with the OpenBazaar 132 | * transaction 133 | * @param threshold The minimum number of signatures required to release 134 | * funds from escrow before the timeout. 135 | * @param timeoutHours The number hours after which the seller can 136 | * unilaterally release funds from escrow. When timeoutHours is set to 0 137 | * it means the seller can never unilaterally release funds from escrow 138 | * @param scriptHash The keccak256 hash of the redeem script. See 139 | * specification for more details 140 | * @param uniqueId A nonce chosen by the buyer 141 | * @dev This call is intended to be made by the buyer and should send the 142 | * amount of ETH to be put in escrow 143 | * @dev You MUST NOT pass a contract address for buyer, seller, or moderator 144 | * or else funds could be locked in this contract permanently. Releasing 145 | * funds from this contract require signatures that cannot be created by 146 | * contract addresses 147 | */ 148 | function addTransaction( 149 | address buyer, 150 | address seller, 151 | address moderator, 152 | uint8 threshold, 153 | uint32 timeoutHours, 154 | bytes32 scriptHash, 155 | bytes20 uniqueId 156 | ) 157 | external 158 | payable 159 | transactionDoesNotExist(scriptHash) 160 | nonZeroAddress(buyer) 161 | nonZeroAddress(seller) 162 | { 163 | _addTransaction( 164 | buyer, 165 | seller, 166 | moderator, 167 | threshold, 168 | timeoutHours, 169 | scriptHash, 170 | msg.value, 171 | uniqueId, 172 | TransactionType.ETHER, 173 | address(0) 174 | ); 175 | 176 | emit Funded(scriptHash, msg.sender, msg.value); 177 | } 178 | 179 | /** 180 | * @notice Registers a new OpenBazaar transaction to the contract 181 | * @dev To be used for moderated ERC20 transactions 182 | * @param buyer The buyer associated with the OpenBazaar transaction 183 | * @param seller The seller associated with the OpenBazaar transaction 184 | * @param moderator The moderator (if any) associated with the OpenBazaar 185 | * transaction 186 | * @param threshold The minimum number of signatures required to release 187 | * funds from escrow before the timeout. 188 | * @param timeoutHours The number hours after which the seller can 189 | * unilaterally release funds from escrow. When timeoutHours is set to 0 190 | * it means the seller can never unilaterally release funds from escrow 191 | * @param scriptHash The keccak256 hash of the redeem script. See 192 | * specification for more details 193 | * @param value The number of tokens to be held in escrow 194 | * @param uniqueId A nonce chosen by the buyer 195 | * @param tokenAddress The address of the ERC20 token contract 196 | * @dev Be sure the buyer approves this contract to spend at least `value` 197 | * on the buyer's behalf 198 | * @dev You MUST NOT pass a contract address for buyer, seller, or moderator 199 | * or else funds could be locked in this contract permanently. Releasing 200 | * funds from this contract require signatures that cannot be created by 201 | * contract addresses 202 | */ 203 | function addTokenTransaction( 204 | address buyer, 205 | address seller, 206 | address moderator, 207 | uint8 threshold, 208 | uint32 timeoutHours, 209 | bytes32 scriptHash, 210 | uint256 value, 211 | bytes20 uniqueId, 212 | address tokenAddress 213 | ) 214 | external 215 | transactionDoesNotExist(scriptHash) 216 | nonZeroAddress(buyer) 217 | nonZeroAddress(seller) 218 | nonZeroAddress(tokenAddress) 219 | { 220 | _addTransaction( 221 | buyer, 222 | seller, 223 | moderator, 224 | threshold, 225 | timeoutHours, 226 | scriptHash, 227 | value, 228 | uniqueId, 229 | TransactionType.TOKEN, 230 | tokenAddress 231 | ); 232 | 233 | ITokenContract token = ITokenContract(tokenAddress); 234 | 235 | emit Funded(scriptHash, msg.sender, value); 236 | 237 | require( 238 | token.transferFrom(msg.sender, address(this), value), 239 | "Token transfer failed, maybe you did not approve escrow contract to spend on behalf of sender" 240 | ); 241 | } 242 | 243 | /** 244 | * @notice Determines whether a given address was a beneficiary of any 245 | * payout from the escrow associated with an OpenBazaar transaction that is 246 | * associated with a given scriptHash 247 | * @param scriptHash scriptHash associated with the OpenBazaar transaction 248 | * of interest 249 | * @param beneficiary Address to be checked 250 | * @return true if and only if the passed address was a beneficiary of some 251 | * payout from the escrow associated with `scriptHash` 252 | */ 253 | function checkBeneficiary( 254 | bytes32 scriptHash, 255 | address beneficiary 256 | ) 257 | external 258 | view 259 | returns (bool) 260 | { 261 | return transactions[scriptHash].beneficiaries[beneficiary]; 262 | } 263 | 264 | /** 265 | * @notice Check whether given party has signed for funds to be released 266 | * from the escrow associated with a scriptHash. 267 | * @param scriptHash Hash identifying the OpenBazaar transaction in question 268 | * @param party The address we are checking 269 | * @return true if and only if `party` received any funds from the escrow 270 | * associated with `scripHash` 271 | */ 272 | function checkVote( 273 | bytes32 scriptHash, 274 | address party 275 | ) 276 | external 277 | view 278 | returns (bool) 279 | { 280 | bool voted = false; 281 | 282 | for (uint256 i = 0; i < transactions[scriptHash].noOfReleases; i++){ 283 | 284 | bytes32 addressHash = keccak256(abi.encodePacked(party, i)); 285 | 286 | if (transactions[scriptHash].voted[addressHash]){ 287 | voted = true; 288 | break; 289 | } 290 | } 291 | 292 | return voted; 293 | } 294 | 295 | /** 296 | * @notice Allows the buyer in an OpenBazaar transaction to add more ETH to 297 | * an existing transaction 298 | * @param scriptHash The scriptHash of the OpenBazaar transaction to which 299 | * funds will be added 300 | */ 301 | function addFundsToTransaction( 302 | bytes32 scriptHash 303 | ) 304 | external 305 | payable 306 | transactionExists(scriptHash) 307 | inFundedState(scriptHash) 308 | checkTransactionType(scriptHash, TransactionType.ETHER) 309 | onlyBuyer(scriptHash) 310 | 311 | { 312 | require(msg.value > 0, "Value must be greater than zero."); 313 | 314 | transactions[scriptHash].value = transactions[scriptHash].value 315 | .add(msg.value); 316 | 317 | emit FundAdded(scriptHash, msg.sender, msg.value); 318 | } 319 | 320 | /** 321 | * @notice Allows the buyer in an OpenBazaar transaction to add more ERC20 322 | * tokens to an existing transaction 323 | * @param scriptHash The scriptHash of the OpenBazaar transaction to which 324 | * funds will be added 325 | * @param value The number of tokens to be added 326 | */ 327 | function addTokensToTransaction( 328 | bytes32 scriptHash, 329 | uint256 value 330 | ) 331 | external 332 | transactionExists(scriptHash) 333 | inFundedState(scriptHash) 334 | checkTransactionType(scriptHash, TransactionType.TOKEN) 335 | onlyBuyer(scriptHash) 336 | { 337 | require(value > 0, "Value must be greater than zero."); 338 | 339 | ITokenContract token = ITokenContract( 340 | transactions[scriptHash].tokenAddress 341 | ); 342 | 343 | transactions[scriptHash].value = transactions[scriptHash].value 344 | .add(value); 345 | 346 | emit FundAdded(scriptHash, msg.sender, value); 347 | 348 | require( 349 | token.transferFrom(msg.sender, address(this), value), 350 | "Token transfer failed, maybe you did not approve the escrow contract to spend on behalf of the buyer" 351 | ); 352 | } 353 | 354 | /** 355 | * @notice Returns an array of scriptHashes associated with trades in which 356 | * a given address was listed as a buyer or a seller 357 | * @param partyAddress The address to look up 358 | * @return an array of scriptHashes 359 | */ 360 | function getAllTransactionsForParty( 361 | address partyAddress 362 | ) 363 | external 364 | view 365 | returns (bytes32[] memory) 366 | { 367 | return partyVsTransaction[partyAddress]; 368 | } 369 | 370 | /** 371 | * @notice This method will be used to release funds from the escrow 372 | * associated with an existing OpenBazaar transaction. 373 | * @dev please see the contract specification for more details 374 | * @param sigV Array containing V component of all the signatures 375 | * @param sigR Array containing R component of all the signatures 376 | * @param sigS Array containing S component of all the signatures 377 | * @param scriptHash ScriptHash of the transaction 378 | * @param destinations List of addresses who will receive funds 379 | * @param amounts List of amounts to be released to the destinations 380 | */ 381 | function execute( 382 | uint8[] calldata sigV, 383 | bytes32[] calldata sigR, 384 | bytes32[] calldata sigS, 385 | bytes32 scriptHash, 386 | address payable[] calldata destinations, 387 | uint256[] calldata amounts 388 | ) 389 | external 390 | transactionExists(scriptHash) 391 | fundsExist(scriptHash) 392 | { 393 | require( 394 | destinations.length > 0, 395 | "Number of destinations must be greater than 0" 396 | ); 397 | require( 398 | destinations.length == amounts.length, 399 | "Number of destinations must match number of values sent" 400 | ); 401 | 402 | _verifyTransaction( 403 | sigV, 404 | sigR, 405 | sigS, 406 | scriptHash, 407 | destinations, 408 | amounts 409 | ); 410 | 411 | transactions[scriptHash].status = Status.RELEASED; 412 | 413 | //solium-disable-next-line security/no-block-members 414 | transactions[scriptHash].lastModified = block.timestamp; 415 | 416 | transactions[scriptHash].noOfReleases = transactions[scriptHash]. 417 | noOfReleases.add(1); 418 | 419 | transactions[scriptHash].released = _transferFunds( 420 | scriptHash, 421 | destinations, 422 | amounts 423 | ).add(transactions[scriptHash].released); 424 | 425 | emit Executed(scriptHash, destinations, amounts); 426 | 427 | require( 428 | transactions[scriptHash].value >= transactions[scriptHash].released, 429 | "Value of transaction should be greater than released value" 430 | ); 431 | } 432 | 433 | /** 434 | * @notice Gives the hash that the parties need to sign in order to 435 | * release funds from the escrow of a given OpenBazaar transactions given 436 | * a set of destinations and amounts 437 | * @param scriptHash Script hash of the OpenBazaar transaction 438 | * @param destinations List of addresses who will receive funds 439 | * @param amounts List of amounts for each destination 440 | * @return a bytes32 hash 441 | */ 442 | function getTransactionHash( 443 | bytes32 scriptHash, 444 | address payable[] memory destinations, 445 | uint256[] memory amounts 446 | ) 447 | public 448 | view 449 | returns (bytes32) 450 | { 451 | bytes32 releaseHash = keccak256( 452 | abi.encode( 453 | keccak256(abi.encodePacked(destinations)), 454 | keccak256(abi.encodePacked(amounts)) 455 | ) 456 | ); 457 | 458 | //follows ERC191 signature scheme: https://github.com/ethereum/EIPs/issues/191 459 | bytes32 txHash = keccak256( 460 | abi.encodePacked( 461 | "\x19Ethereum Signed Message:\n32", 462 | keccak256( 463 | abi.encodePacked( 464 | byte(0x19), 465 | byte(0), 466 | address(this), 467 | releaseHash, 468 | transactions[scriptHash].noOfReleases, 469 | scriptHash 470 | ) 471 | ) 472 | ) 473 | ); 474 | return txHash; 475 | } 476 | 477 | /** 478 | * @notice Calculating scriptHash for a given OpenBazaar transaction 479 | * @param uniqueId A nonce chosen by the buyer 480 | * @param threshold The minimum number of signatures required to release 481 | * funds from escrow before the timeout. 482 | * @param timeoutHours The number hours after which the seller can 483 | * unilaterally release funds from escrow. When timeoutHours is set to 0 484 | * it means the seller can never unilaterally release funds from escrow 485 | * @param buyer The buyer associated with the OpenBazaar transaction 486 | * @param seller The seller associated with the OpenBazaar transaction 487 | * @param moderator The moderator (if any) associated with the OpenBazaar 488 | * transaction 489 | * @param tokenAddress The address of the ERC20 token contract 490 | * @return a bytes32 hash 491 | */ 492 | function calculateRedeemScriptHash( 493 | bytes20 uniqueId, 494 | uint8 threshold, 495 | uint32 timeoutHours, 496 | address buyer, 497 | address seller, 498 | address moderator, 499 | address tokenAddress 500 | ) 501 | public 502 | view 503 | returns (bytes32) 504 | { 505 | if (tokenAddress == address(0)) { 506 | return keccak256( 507 | abi.encodePacked( 508 | uniqueId, 509 | threshold, 510 | timeoutHours, 511 | buyer, 512 | seller, 513 | moderator, 514 | address(this) 515 | ) 516 | ); 517 | } else { 518 | return keccak256( 519 | abi.encodePacked( 520 | uniqueId, 521 | threshold, 522 | timeoutHours, 523 | buyer, 524 | seller, 525 | moderator, 526 | address(this), 527 | tokenAddress 528 | ) 529 | ); 530 | } 531 | } 532 | 533 | /** 534 | * @notice This methods checks validity of a set of signatures AND whether 535 | * they are sufficient to release funds from escrow 536 | * @param sigV Array containing V component of all the signatures 537 | * @param sigR Array containing R component of all the signatures 538 | * @param sigS Array containing S component of all the signatures 539 | * @param scriptHash ScriptHash of the transaction 540 | * @param destinations List of addresses who will receive funds 541 | * @param amounts List of amounts to be released to the destinations 542 | * @dev This will revert if the set of signatures is not valid or the 543 | * attempted payout is not valid. It will succeed silently otherwise 544 | */ 545 | function _verifyTransaction( 546 | uint8[] memory sigV, 547 | bytes32[] memory sigR, 548 | bytes32[] memory sigS, 549 | bytes32 scriptHash, 550 | address payable[] memory destinations, 551 | uint256[] memory amounts 552 | ) 553 | private 554 | { 555 | _verifySignatures( 556 | sigV, 557 | sigR, 558 | sigS, 559 | scriptHash, 560 | destinations, 561 | amounts 562 | ); 563 | 564 | bool timeLockExpired = _isTimeLockExpired( 565 | transactions[scriptHash].timeoutHours, 566 | transactions[scriptHash].lastModified 567 | ); 568 | 569 | //if the minimum number (`threshold`) of signatures are not present and 570 | //either the timelock has not expired or the release was not signed by 571 | //the seller then revert 572 | if (sigV.length < transactions[scriptHash].threshold) { 573 | if (!timeLockExpired) { 574 | revert("Min number of sigs not present and timelock not expired"); 575 | } 576 | else if ( 577 | !transactions[scriptHash].voted[keccak256( 578 | abi.encodePacked( 579 | transactions[scriptHash].seller, 580 | transactions[scriptHash].noOfReleases 581 | ) 582 | )] 583 | ) 584 | { 585 | revert("Min number of sigs not present and seller did not sign"); 586 | } 587 | } 588 | } 589 | 590 | /** 591 | * @notice Method to transfer funds to a set of destinations 592 | * @param scriptHash Hash identifying the OpenBazaar transaction 593 | * @param destinations List of addresses who will receive funds 594 | * @param amounts List of amounts to be released to the destinations 595 | * @return the total amount of funds that were paid out 596 | */ 597 | function _transferFunds( 598 | bytes32 scriptHash, 599 | address payable[] memory destinations, 600 | uint256[] memory amounts 601 | ) 602 | private 603 | returns (uint256) 604 | { 605 | Transaction storage t = transactions[scriptHash]; 606 | 607 | uint256 valueTransferred = 0; 608 | 609 | if (t.transactionType == TransactionType.ETHER) { 610 | for (uint256 i = 0; i < destinations.length; i++) { 611 | 612 | require( 613 | destinations[i] != address(0), 614 | "zero address is not allowed as destination address" 615 | ); 616 | 617 | require( 618 | t.isOwner[destinations[i]], 619 | "Destination address is not one of the owners" 620 | ); 621 | 622 | require( 623 | amounts[i] > 0, 624 | "Amount to be sent should be greater than 0" 625 | ); 626 | 627 | valueTransferred = valueTransferred.add(amounts[i]); 628 | 629 | //add receiver as beneficiary 630 | t.beneficiaries[destinations[i]] = true; 631 | destinations[i].transfer(amounts[i]); 632 | } 633 | 634 | } else if (t.transactionType == TransactionType.TOKEN) { 635 | 636 | ITokenContract token = ITokenContract(t.tokenAddress); 637 | 638 | for (uint256 j = 0; j < destinations.length; j++) { 639 | 640 | require( 641 | destinations[j] != address(0), 642 | "zero address is not allowed as destination address" 643 | ); 644 | 645 | require( 646 | t.isOwner[destinations[j]], 647 | "Destination address is not one of the owners" 648 | ); 649 | 650 | require( 651 | amounts[j] > 0, 652 | "Amount to be sent should be greater than 0" 653 | ); 654 | 655 | valueTransferred = valueTransferred.add(amounts[j]); 656 | 657 | //add receiver as beneficiary 658 | t.beneficiaries[destinations[j]] = true; 659 | 660 | require( 661 | token.transfer(destinations[j], amounts[j]), 662 | "Token transfer failed." 663 | ); 664 | } 665 | } 666 | return valueTransferred; 667 | } 668 | 669 | /** 670 | * @notice Checks whether a given set of signatures are valid 671 | * @param sigV Array containing V component of all the signatures 672 | * @param sigR Array containing R component of all the signatures 673 | * @param sigS Array containing S component of all the signatures 674 | * @param scriptHash ScriptHash of the transaction 675 | * @param destinations List of addresses who will receive funds 676 | * @param amounts List of amounts to be released to the destinations 677 | * @dev This also records which addresses have successfully signed 678 | * @dev This function SHOULD NOT be called by ANY function other than 679 | * `_verifyTransaction` 680 | */ 681 | function _verifySignatures( 682 | uint8[] memory sigV, 683 | bytes32[] memory sigR, 684 | bytes32[] memory sigS, 685 | bytes32 scriptHash, 686 | address payable[] memory destinations, 687 | uint256[] memory amounts 688 | ) 689 | private 690 | { 691 | require(sigR.length == sigS.length, "R,S length mismatch"); 692 | require(sigR.length == sigV.length, "R,V length mismatch"); 693 | 694 | bytes32 txHash = getTransactionHash( 695 | scriptHash, 696 | destinations, 697 | amounts 698 | ); 699 | 700 | for (uint256 i = 0; i < sigR.length; i++) { 701 | 702 | address recovered = ecrecover( 703 | txHash, 704 | sigV[i], 705 | sigR[i], 706 | sigS[i] 707 | ); 708 | 709 | bytes32 addressHash = keccak256( 710 | abi.encodePacked( 711 | recovered, 712 | transactions[scriptHash].noOfReleases 713 | ) 714 | ); 715 | 716 | require( 717 | transactions[scriptHash].isOwner[recovered], 718 | "Invalid signature" 719 | ); 720 | require( 721 | !transactions[scriptHash].voted[addressHash], 722 | "Same signature sent twice" 723 | ); 724 | transactions[scriptHash].voted[addressHash] = true; 725 | } 726 | } 727 | 728 | /** 729 | * @notice Checks whether a timeout has occured 730 | * @param timeoutHours The number hours after which the seller can 731 | * unilaterally release funds from escrow. When `timeoutHours` is set to 0 732 | * it means the seller can never unilaterally release funds from escrow 733 | * @param lastModified The timestamp of the last modification of escrow for 734 | * a particular OpenBazaar transaction 735 | * @return true if and only if `timeoutHours` hours have passed since 736 | * `lastModified` 737 | */ 738 | function _isTimeLockExpired( 739 | uint32 timeoutHours, 740 | uint256 lastModified 741 | ) 742 | private 743 | view 744 | returns (bool) 745 | { 746 | //solium-disable-next-line security/no-block-members 747 | uint256 timeSince = block.timestamp.sub(lastModified); 748 | return ( 749 | timeoutHours == 0 ? false : timeSince > uint256(timeoutHours).mul(1 hours) 750 | ); 751 | } 752 | 753 | /** 754 | * @dev Private method for adding a new OpenBazaar transaction to the 755 | * contract. Used to reduce code redundancy 756 | * @param buyer The buyer associated with the OpenBazaar transaction 757 | * @param seller The seller associated with the OpenBazaar transaction 758 | * @param moderator The moderator (if any) associated with the OpenBazaar 759 | * transaction 760 | * @param threshold The minimum number of signatures required to release 761 | * funds from escrow before the timeout. 762 | * @param timeoutHours The number hours after which the seller can 763 | * unilaterally release funds from escrow. When timeoutHours is set to 0 764 | * it means the seller can never unilaterally release funds from escrow 765 | * @param scriptHash The keccak256 hash of the redeem script. See 766 | * specification for more details 767 | * @param value The amount of currency to add to escrow 768 | * @param uniqueId A nonce chosen by the buyer 769 | * @param transactionType Indicates whether the OpenBazaar trade is using 770 | * ETH or ERC20 tokens for payment 771 | * @param tokenAddress The address of the ERC20 token being used for 772 | * payment. Set to 0 if the OpenBazaar transaction is settling in ETH 773 | */ 774 | function _addTransaction( 775 | address buyer, 776 | address seller, 777 | address moderator, 778 | uint8 threshold, 779 | uint32 timeoutHours, 780 | bytes32 scriptHash, 781 | uint256 value, 782 | bytes20 uniqueId, 783 | TransactionType transactionType, 784 | address tokenAddress 785 | ) 786 | private 787 | { 788 | require(buyer != seller, "Buyer and seller are same"); 789 | require(value > 0, "Value passed is 0"); 790 | require(threshold > 0, "Threshold must be greater than 0"); 791 | require(threshold <= 3, "Threshold must not be greater than 3"); 792 | 793 | //when threshold is 1 that indicates the OpenBazaar transaction is not 794 | //being moderated, so `moderator` can be any address 795 | //if `threadhold > 1` then `moderator` should be nonzero address 796 | require( 797 | threshold == 1 || moderator != address(0), 798 | "Either threshold should be 1 or valid moderator address should be passed" 799 | ); 800 | 801 | require( 802 | scriptHash == calculateRedeemScriptHash( 803 | uniqueId, 804 | threshold, 805 | timeoutHours, 806 | buyer, 807 | seller, 808 | moderator, 809 | tokenAddress 810 | ), 811 | "Calculated script hash does not match passed script hash." 812 | ); 813 | 814 | transactions[scriptHash] = Transaction({ 815 | buyer: buyer, 816 | seller: seller, 817 | moderator: moderator, 818 | value: value, 819 | status: Status.FUNDED, 820 | //solium-disable-next-line security/no-block-members 821 | lastModified: block.timestamp, 822 | threshold: threshold, 823 | timeoutHours: timeoutHours, 824 | transactionType:transactionType, 825 | tokenAddress:tokenAddress, 826 | released: uint256(0), 827 | noOfReleases: uint256(0) 828 | }); 829 | 830 | transactions[scriptHash].isOwner[seller] = true; 831 | transactions[scriptHash].isOwner[buyer] = true; 832 | 833 | //check if buyer or seller are not passed as moderator 834 | require( 835 | !transactions[scriptHash].isOwner[moderator], 836 | "Either buyer or seller is passed as moderator" 837 | ); 838 | 839 | //the moderator should be an owner only if `threshold > 1` 840 | if (threshold > 1) { 841 | transactions[scriptHash].isOwner[moderator] = true; 842 | } 843 | 844 | transactionCount++; 845 | 846 | partyVsTransaction[buyer].push(scriptHash); 847 | partyVsTransaction[seller].push(scriptHash); 848 | } 849 | } 850 | -------------------------------------------------------------------------------- /contracts/escrow/EscrowProxy.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.5.7; 2 | 3 | import "./IEscrow.sol"; 4 | 5 | 6 | /** 7 | * @title Escrow Proxy 8 | * @author OB1 9 | * @notice This a proxy contract used to return hashes that need to be signed in 10 | * order for funds to be released from a given version of the escrow contract 11 | * @dev For v1.0.0 of the escrow contract this proxy will generate the hash 12 | * that needs to be signed in order to release funds from escrow. For all later 13 | * versions of the escrow contract, the escrow contract itself should have such 14 | * a function. 15 | */ 16 | contract EscrowProxy { 17 | 18 | address public legacyEscrowVersion; 19 | 20 | constructor(address _legacyEscrowVersion) public { 21 | //the zero address is allowed 22 | legacyEscrowVersion = _legacyEscrowVersion; 23 | } 24 | 25 | /** 26 | * @notice Gets the hash that must be signed to release funds from escrow 27 | * for a given OpenBazaar transaction, set of destinations, set of amounts, 28 | * and version of the escrow contract 29 | * @param escrowVersion The address of the escrow contract being used for 30 | * the OpenBazaar transaction in question 31 | * @param scriptHash The scriptHash of the OpenBazaar transaction 32 | * @param destinations List of addresses who will receive funds 33 | * @param amounts List of amounts to be released to the destinations 34 | * @return a bytes32 hash to sign 35 | */ 36 | function getTransactionHash( 37 | address escrowVersion, 38 | bytes32 scriptHash, 39 | address[] calldata destinations, 40 | uint256[] calldata amounts 41 | ) 42 | external 43 | view 44 | returns (bytes32) 45 | { 46 | require( 47 | escrowVersion != address(0), 48 | "Invalid escrow contract version!!" 49 | ); 50 | 51 | if (escrowVersion == legacyEscrowVersion) { 52 | return _legacyEscrowTxHash( 53 | scriptHash, 54 | destinations, 55 | amounts 56 | ); 57 | } else { 58 | IEscrow escrow = IEscrow(escrowVersion); 59 | 60 | return escrow.getTransactionHash( 61 | scriptHash, 62 | destinations, 63 | amounts 64 | ); 65 | } 66 | } 67 | 68 | /** 69 | * @notice Gets the hash that must be signed to release funds from escrow 70 | * for a given OpenBazaar transaction, set of destinations, set of amounts, 71 | * and version 1.0.0 of the escrow contract 72 | * @param scriptHash The scriptHash of the OpenBazaar transaction 73 | * @param destinations List of addresses who will receive funds 74 | * @param amounts List of amounts to be released to the destinations 75 | * @return a bytes32 hash to sign 76 | */ 77 | function _legacyEscrowTxHash( 78 | bytes32 scriptHash, 79 | address[] memory destinations, 80 | uint256[] memory amounts 81 | ) 82 | private 83 | view 84 | returns (bytes32) 85 | { 86 | bytes32 txHash = keccak256( 87 | abi.encodePacked( 88 | "\x19Ethereum Signed Message:\n32", 89 | keccak256( 90 | abi.encodePacked( 91 | byte(0x19), 92 | byte(0), 93 | legacyEscrowVersion, 94 | destinations, 95 | amounts, 96 | scriptHash 97 | ) 98 | ) 99 | ) 100 | ); 101 | 102 | return txHash; 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /contracts/escrow/EscrowSpec.md: -------------------------------------------------------------------------------- 1 | # Escrow Contract Specification 2 | 3 | ## Introduction 4 | 5 | OpenBazaar facilitates trades between arbitrary third parties on the internet. Currently, only UTXO-based cryptocurrencies can be used as a medium of exchange on OpenBazaar. The escrow contract is intended to be used as a way to shoehorn Ethereum functionality into OpenBazaar's existing framework so that users can trade using ETH and ERC20 tokens as their medium of exchange. 6 | 7 | **IMPORTANT:** This contract supports only ETH and _compliant_ ERC20 tokens. Use of the Escrow contract with non-compliant ERC20 tokens may result in permanent loss of tokens. In particular, if the token does not return `true` upon a successful call to `token.transfer` or `token.transferFrom` you should not use the token with this escrow contract. See [this article](https://medium.com/coinmonks/missing-return-value-bug-at-least-130-tokens-affected-d67bf08521ca) for a deeper explanation. We will never present non-complaint tokens as a payment option in the OpenBazaar UI, but it is still possible to send (and permanently lose) such tokens by interacting with the Escrow contract through a third-party UI. 8 | 9 | ### How OpenBazaar Trades Currently Work (in UTXO land) 10 | 11 | #### Moderated Payments 12 | 13 | When a buyer and seller have agreed on a product and a price, the buyer sends their funds to an escrow address, which is a 2-of-3 multisig address with one key controlled by the buyer, one key controlled by the seller, and one key controlled by a moderator that has been agreed upon by both the buyer and the seller. 14 | 15 | **IMPORTANT:** This contract requires _signatures_ in order to release funds from escrow. Contracts cannot create signatures corresponding to their own addresses. Therefore, you SHOULD NOT pass a contract address for the `buyer`, `seller`, or `moderator`. Doing so could make it impossible for your funds to be released from escrow. 16 | 17 | On the "happy path", the seller delivers the goods, then the buyer releases the funds to the seller (with the buyer and seller signing the payout txn from the escrow address). 18 | 19 | In the event that the seller does not deliver the goods as promised, the buyer pleads their case to the moderator, and the buyer & moderator can send the funds from escrow back to the buyer. 20 | 21 | In the (very common) case where the buyer receives their goods but doesn't release the funds to the seller, the seller pleads their case to the moderator, and the seller & moderator sign the funds from escrow to the seller. 22 | 23 | The seller can also unilaterally release funds from escrow after a previously agreed upon amount of time has passed. This allows the seller to release the funds from escrow without the moderator in the event that the buyer disappears. With UTXO-based coins, this is achieved by requiring that the buyer sign an nLockTime transaction releasing funds to the seller, and then passing that txn to the seller (off-chain) before the seller delivers the product or service. 24 | 25 | #### Direct Payments 26 | 27 | Buyers have the option of _not_ using a moderator when making an OpenBazaar trade. While this isn't recommended, it may be an acceptable risk for the buyer if the buyer trusts the seller. Direct/unmoderated payments come in two forms: online payments and offline payments. 28 | 29 | Online direct payments occur when the buyer knows the seller is online. For online payments, the buyer simply sends the funds directly to the sellers wallet. These are simple, classic transfers of value from one account to another. 30 | 31 | Offline payments occur when the buyer sees that the seller is offline and is _uncertain_ whether the seller will ever come back online. In this case the buyer sends the funds to a 1-of-2 multisig address with one key held by the buyer and the other held by the seller. If the seller comes back online, they can accept the funds. If the seller doesn't come back online, the buyer can reclaim the funds. 32 | 33 | ### Limitations Imposed by OpenBazaar's Wallet Interface 34 | 35 | OpenBazaar interacts with all supported coins through its [wallet interface](https://github.com/OpenBazaar/wallet-interface/blob/master/wallet.go#L77). This means that OpenBazaar's Ethereum smart contracts must be designed in such a way as to be compatible with that interface. OpenBazaar is a live/launched product, so making big changes to the wallet interface in order to support Ethereum is non-trivial. Instead, we've decided to keep the wallet interface fixed (for now), and design the smart contract to be compatible with it. 36 | 37 | ## Intended Use of the Escrow contract 38 | 39 | The Escrow contract will store the escrowed funds and state information for _every_ OpenBazaar trade that is using Ethereum (or ERC20 tokens) as the medium of exchange. (We could have, instead, opted to deploy a new escrow contract for each Ethereum-based trade -- thereby siloing escrowed funds from each trade in their own smart contract. However, we think the gas requirements for doing so are cost prohibitive, and we fear that would introduce too much friction into Ethereum-based trades). OpenBazaar trades that use ETH/ERC20 as the medium of exchange are intended to follow the same protocol as those that use a UTXO-based coin as the medium of exchange -- and the escrow smart contract is intended to facilitate that. 40 | 41 | ### Funding the Trade 42 | 43 | Buyers initiate a trade by creating/storing a _Transaction_ struct in the Escrow contract and (simultaneously) funding the transaction by sending ETH (or ERC20 tokens) to the Escrow contract. At this point the transaction is in the _FUNDED_ state. While in the _FUNDED_ state, the buyer may add more ETH (or ERC20 tokens) to escrow if necessary. Adding more funds to escrow _does not_ result in any changes to _timeoutHours_ (see next section). 44 | 45 | ### Releasing Funds from Escrow 46 | 47 | While the transaction is in the _FUNDED_ state, the escrowed funds can be released only if: (1) Two of the three participants (buyer, seller, and moderator) agree on how the escrowed funds are to be distributed, or (2) an amount of time (_timeoutHours_) has passed since the last time the buyer added funds to escrow. (Note: when _timeoutHours_ is set to 0, this indicates an infinite timeout, not an instantaneous timeout. In other words, if _timeoutHours_ is set to 0 then the seller can never unilaterally release funds from escrow.) 48 | 49 | The reasoning behind (2) is that it is very common for buyers to not release funds after they've received their goods (this is due more to buyer laziness than malice). In that event, we want to make it easy for the seller to claim the escrowed funds without having to coordinate with a moderator. 50 | 51 | Funds released from escrow can be split up and sent to various addresses. However, the receiving addresses _must be_ the addresses of the trade's buyer, seller, or moderator. To reiterate, funds cannot be sent to an address that is not affiliated with the trade in question, but the escrowed funds can be divided up among the participants in any way -- so long as 2-of-3 of the parties agree. 52 | 53 | Upon release of funds from escrow, the trade is put into the _RELEASED_ state. Once in the _RELEASED_ state, trades can no longer be altered. All participants who received some of the escrowed funds are noted in the trade's _Transaction_ struct (via the _beneficiaries_ mapping). 54 | 55 | (The _beneficiaries_ information will be used later, by other contracts, to determine whether or not a given trade was disputed, refunded, etc). 56 | 57 | If there are ever any funds left in escrow (even if the trade is in the _RELEASED_ state) the party's can call _execute_ to release the funds. 58 | 59 | ### Offline Direct Payments 60 | 61 | The escrow contract can mirror the behavior of UTXO-based offline payments by calling `addTransaction` (or `addTokenTransaction` if it is an ERC20 transaction), setting the `threshold` value to 1, and setting the moderator address to a known, non-zero burn address. The effect is the equivalent of a 1-of-2 multisig address where the buyer holds one key and the seller holds the other. 62 | 63 | ## Known Issues / Misc 64 | 65 | It is assumed that the moderator is trusted by both the buyer and the seller before the trade begins. The obvious threat of collusion between a buyer and moderator -- or seller and moderator -- is beyond the scope of this contract. 66 | 67 | The _transferFunds_ function uses push payments (rather than the pull model) due to limitations imposed by OpenBazaar's wallet interface. Hence any of the beneficiaries of a payout from escrow can cause the payout to fail (for example, by putting a _revert()_ in their fallback function). Game theoretically speaking, such a DoS attack is irrational for any of the participants capable of causing such an issue, because the honest parties can always benefit by removing the offending party as a beneficiary and taking her share of the payout. 68 | 69 | (For example, suppose the three parties agreed that the moderator would received 5% of the funds, and that the buyer and seller would split the remaining funds. The seller, being unhappy with the result, could cause the payout to fail until she could negotiate a more favorable agreement. However, the buyer & moderator -- upon seeing the seller's misbehavior -- could simply agree to remove the seller as a beneficiary -- thus removing the seller's ability to DoS the payout.) 70 | 71 | For this reason, we consider the DoS possibility caused by use of push payments in the _transferFunds_ function to be low risk. 72 | -------------------------------------------------------------------------------- /contracts/escrow/IEscrow.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.5.7; 2 | 3 | interface IEscrow { 4 | 5 | function transactions( 6 | bytes32 _scriptHash 7 | ) 8 | external view 9 | returns( 10 | uint256 value, 11 | uint256 lastModified, 12 | uint8 status, 13 | uint8 transactionType, 14 | uint8 threshold, 15 | uint32 timeoutHours, 16 | address buyer, 17 | address seller, 18 | address tokenAddress 19 | ); 20 | 21 | function addTransaction( 22 | address buyer, 23 | address seller, 24 | address moderator, 25 | uint8 threshold, 26 | uint32 timeoutHours, 27 | bytes32 scriptHash, 28 | bytes20 uniqueId 29 | ) 30 | external payable; 31 | 32 | function addTokenTransaction( 33 | address buyer, 34 | address seller, 35 | address moderator, 36 | uint8 threshold, 37 | uint32 timeoutHours, 38 | bytes32 scriptHash, 39 | uint256 value, 40 | bytes20 uniqueId, 41 | address tokenAddress 42 | ) 43 | external; 44 | 45 | function addFundsToTransaction(bytes32 scriptHash) external payable; 46 | 47 | function addTokensToTransaction( 48 | bytes32 scriptHash, 49 | uint256 value 50 | ) 51 | external; 52 | 53 | function execute( 54 | uint8[] calldata sigV, 55 | bytes32[] calldata sigR, 56 | bytes32[] calldata sigS, 57 | bytes32 scriptHash, 58 | address[] calldata destinations, 59 | uint256[] calldata amounts 60 | ) 61 | external; 62 | 63 | function checkBeneficiary( 64 | bytes32 scriptHash, 65 | address beneficiary 66 | ) 67 | external 68 | view 69 | returns (bool); 70 | 71 | function checkVote( 72 | bytes32 scriptHash, 73 | address party 74 | ) 75 | external 76 | view 77 | returns (bool); 78 | 79 | function getTransactionHash( 80 | bytes32 scriptHash, 81 | address[] calldata destinations, 82 | uint256[] calldata amounts 83 | ) 84 | external 85 | view 86 | returns (bytes32); 87 | 88 | function getAllTransactionsForParty( 89 | address partyAddress 90 | ) 91 | external 92 | view 93 | returns (bytes32[] memory); 94 | 95 | function calculateRedeemScriptHash( 96 | bytes20 uniqueId, 97 | uint8 threshold, 98 | uint32 timeoutHours, 99 | address buyer, 100 | address seller, 101 | address moderator, 102 | address tokenAddress 103 | ) 104 | external 105 | view 106 | returns (bytes32); 107 | } 108 | -------------------------------------------------------------------------------- /contracts/powerUps/PowerUps.sol: -------------------------------------------------------------------------------- 1 | /* solium-disable security/no-block-members */ 2 | 3 | pragma solidity 0.5.7; 4 | 5 | import "openzeppelin-solidity/contracts/math/SafeMath.sol"; 6 | import "../token/ITokenContract.sol"; 7 | 8 | 9 | /** 10 | * @dev 'Powering-up a listing' is spending OpenBazaar tokens to advertise a 11 | * listing in one of the OpenBazaar clients. 12 | */ 13 | contract PowerUps { 14 | 15 | using SafeMath for uint256; 16 | 17 | ITokenContract public token; 18 | 19 | struct PowerUp { 20 | string contentAddress; // IPFS/IPNS address, peerID, etc 21 | uint256 tokensBurned; // total tokens burned towards this PowerUp 22 | uint256 lastTopupTime; // last time tokens were burned for this PowerUp 23 | bytes32 keyword; // search term, reserved keyword, etc 24 | } 25 | 26 | PowerUp[] powerUps; // stores PowerUps 27 | 28 | mapping(bytes32 => uint256[]) keywordVsPowerUpIds; 29 | 30 | event NewPowerUpAdded( 31 | address indexed initiator, 32 | uint256 id, // the index of this PowerUp in the powerUps[] array 33 | uint256 tokensBurned 34 | ); 35 | 36 | event Topup( 37 | address indexed initiator, 38 | uint256 id, // the index of the PowerUp in the powerUps[] array 39 | uint256 tokensBurned 40 | ); 41 | 42 | modifier powerUpExists(uint256 id) { 43 | require(id <= powerUps.length.sub(1), "PowerUp does not exists"); 44 | _; 45 | } 46 | 47 | modifier nonZeroAddress(address addressToCheck) { 48 | require(addressToCheck != address(0), "Zero address passed"); 49 | _; 50 | } 51 | 52 | constructor(address obTokenAddress) public nonZeroAddress(obTokenAddress) { 53 | token = ITokenContract(obTokenAddress); 54 | } 55 | 56 | /** 57 | * @dev Add new PowerUp 58 | * @param contentAddress IPFS/IPNS address, peerID, etc 59 | * @param amount Amount of tokens to burn 60 | * @param keyword Bytes32 search term, reserved keyword, etc 61 | * @return id Index of the PowerUp in the powerUps[] array 62 | */ 63 | function addPowerUp( 64 | string calldata contentAddress, 65 | uint256 amount, 66 | bytes32 keyword 67 | ) 68 | external 69 | returns (uint256 id) 70 | { 71 | 72 | require(bytes(contentAddress).length > 0, "Content Address is empty"); 73 | id = _addPowerUp(contentAddress, amount, keyword); 74 | 75 | return id; 76 | } 77 | 78 | /** 79 | * @dev Add multiple PowerUps for different keywords but the same 80 | * content address 81 | * This is intended to be used for associating a given contentAddress with 82 | * multiple search keywords by batching the PowerUps together. This is 83 | * simply to allow the creation of multiple PowerUps with a single function 84 | * call. 85 | * @param contentAddress IPFS/IPNS address of the listing 86 | * @param amounts Amount of tokens to be burnt for each PowerUp 87 | * @param keywords Keywords in bytes32 form for each PowerUp 88 | * Be sure to keep arrays small enough so as not to exceed the block gas 89 | * limit. 90 | */ 91 | function addPowerUps( 92 | string calldata contentAddress, 93 | uint256[] calldata amounts, 94 | bytes32[] calldata keywords 95 | ) 96 | external 97 | returns (uint256[] memory ids) 98 | { 99 | 100 | require( 101 | bytes(contentAddress).length > 0, 102 | "Content Address is empty" 103 | ); 104 | 105 | require( 106 | amounts.length == keywords.length, 107 | "keywords and amounts length mismatch" 108 | ); 109 | 110 | ids = new uint256[](amounts.length); 111 | 112 | for (uint256 i = 0; i < amounts.length; i++) { 113 | ids[i] = _addPowerUp(contentAddress, amounts[i], keywords[i]); 114 | } 115 | 116 | return ids; 117 | } 118 | 119 | /** 120 | * @dev Topup a PowerUp's balance (that is, burn more tokens in association 121 | * with am existing PowerUp) 122 | * @param id The index of the PowerUp in the powerUps array 123 | * @param amount Amount of tokens to burn 124 | */ 125 | function topUpPowerUp( 126 | uint256 id, 127 | uint256 amount 128 | ) 129 | external 130 | powerUpExists(id) 131 | { 132 | 133 | require( 134 | amount > 0, 135 | "Amount of tokens to burn should be greater than 0" 136 | ); 137 | 138 | powerUps[id].tokensBurned = powerUps[id].tokensBurned.add(amount); 139 | 140 | powerUps[id].lastTopupTime = block.timestamp; 141 | 142 | token.burnFrom(msg.sender, amount); 143 | 144 | emit Topup(msg.sender, id, amount); 145 | } 146 | 147 | /** 148 | * @dev Returns info about a given PowerUp 149 | * @param id The index of the PowerUp in the powerUps array 150 | */ 151 | function getPowerUpInfo( 152 | uint256 id 153 | ) 154 | external 155 | view 156 | returns ( 157 | string memory contentAddress, 158 | uint256 tokensBurned, 159 | uint256 lastTopupTime, 160 | bytes32 keyword 161 | ) 162 | { 163 | if (powerUps.length > id) { 164 | 165 | PowerUp storage powerUp = powerUps[id]; 166 | 167 | contentAddress = powerUp.contentAddress; 168 | tokensBurned = powerUp.tokensBurned; 169 | lastTopupTime = powerUp.lastTopupTime; 170 | keyword = powerUp.keyword; 171 | 172 | } 173 | 174 | return (contentAddress, tokensBurned, lastTopupTime, keyword); 175 | } 176 | 177 | /** 178 | * @dev returns how many powerups are available for the given keyword 179 | * @param keyword Keyword for which the result needs to be fetched 180 | */ 181 | function noOfPowerUps(bytes32 keyword) 182 | external 183 | view 184 | returns (uint256 count) 185 | { 186 | count = keywordVsPowerUpIds[keyword].length; 187 | 188 | return count; 189 | } 190 | 191 | /** 192 | * @dev returns the id (index in the powerUps[] array) of the PowerUp 193 | * refered to by the `index`th element of a given `keyword` array. 194 | * ie: getPowerUpIdAtIndex("shoes",23) will return the id of the 23rd 195 | * PowerUp that burned tokens in association with the keyword "shoes". 196 | * @param keyword Keyword string for which the PowerUp ids will be fetched 197 | * @param index Index at which id of the PowerUp needs to be fetched 198 | */ 199 | function getPowerUpIdAtIndex( 200 | bytes32 keyword, 201 | uint256 index 202 | ) 203 | external 204 | view 205 | returns (uint256 id) 206 | { 207 | 208 | require( 209 | keywordVsPowerUpIds[keyword].length > index, 210 | "Array index out of bounds" 211 | ); 212 | 213 | id = keywordVsPowerUpIds[keyword][index]; 214 | 215 | return id; 216 | } 217 | 218 | /** 219 | * @dev This method will return an array of ids of PowerUps associated with 220 | * the given keyword 221 | * @param keyword The keyword for which the array of PowerUps will be fetched 222 | */ 223 | function getPowerUpIds(bytes32 keyword) 224 | external 225 | view 226 | returns (uint256[] memory ids) 227 | { 228 | 229 | ids = keywordVsPowerUpIds[keyword]; 230 | 231 | return ids; 232 | 233 | } 234 | 235 | //private helper 236 | function _addPowerUp( 237 | string memory contentAddress, 238 | uint256 amount, 239 | bytes32 keyword 240 | ) 241 | private 242 | returns (uint256 id) 243 | { 244 | 245 | require( 246 | amount > 0, 247 | "Amount of tokens to burn should be greater than 0" 248 | ); 249 | 250 | powerUps.push( 251 | PowerUp({ 252 | contentAddress:contentAddress, 253 | tokensBurned:amount, 254 | lastTopupTime:block.timestamp, 255 | keyword: keyword 256 | }) 257 | ); 258 | 259 | keywordVsPowerUpIds[keyword].push(powerUps.length.sub(1)); 260 | 261 | token.burnFrom(msg.sender, amount); 262 | 263 | emit NewPowerUpAdded(msg.sender, powerUps.length.sub(1), amount); 264 | 265 | id = powerUps.length.sub(1); 266 | 267 | return id; 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /contracts/powerUps/PowerUpsSpec.md: -------------------------------------------------------------------------------- 1 | # PowerUps Contract Specification 2 | 3 | ## Introduction 4 | 5 | We want to allow users to burn tokens in order to: 6 | 7 | 1. Have a listing appear in the top results of a keyword search in one of the OB clients 8 | 2. Have a listing appear in a special section of one of the OB clients (for example, on the front page above the fold) 9 | 3. Have some visual indicator on their store that indicates they've burned some tokens in association with their store (which signals that the store is unlikely to be one of a huge number of sybils) 10 | 11 | All three of these are handled using a single `PowerUps` contract. 12 | 13 | ## Intended Functionality 14 | 15 | The `PowerUps` contract stores a list of "PowerUps", which is a struct with 4 properties: 16 | 17 | 1. A `contentAddress` (string), which is either the IPFS/IPNS address of an OB listing or the peerID of a store on the OB network. (The contract does NOT need to verify that the string is a valid IPFS/IPNS address). 18 | 2. An integer `tokensBurned` (uint256), which indicates the total number of tokens that have been burned in association with this PowerUp. 19 | 3. A timestamp `lastTopupTime` (uint256), which indicates the last time funds were burned in association with this PowerUp. 20 | 4. A `keyword` (bytes32), which is a keyword that is used to indicate the purpose of the PowerUp, Ie: `keyword = web3.utils.fromAscii("kw:shoes")` may indicate that the PowerUp is being used to have a listing appear in the search results when a user searches for "shoes" in the OB client. Ie: `keyword = web3.utils.fromAscii("pl:mc-fp-af")` may indicate that the PowerUp is being used to have a listing appear in the **m**obile **c**lient, on the **f**ront **p**age, **a**bove the **f**old. Ie: `keyword = web3.utils.fromAscii("ps:")` may indicate that the PowerUp is being used to have an non-sybil indicator put on a store. Etc. 21 | 22 | Users can create a new PowerUp by calling `addPowerUp`. They can burn more tokens towards an existing PowerUp by calling `topUpPowerUp`. 23 | 24 | They can add multiple PowerUps at once (with the restriction that the `contentAddress` be the same for each PowerUp) by calling `addPowerUps`. 25 | 26 | ## Additional Notes 27 | 28 | - Multiple PowerUps may exist that have the same `contentAddress` and/or `keyword`. There is no uniqueness requirement here. 29 | - It is not required that the creator of a new PowerUp be in control of the IPFS/IPNS/peerID stored in the `contentAddress` of the PowerUp. 30 | - It is not necessary to check (at the contract level) whether `contentAddress` is actually a valid IPFS/IPNS/peerID (this will be done client side and any "bad" PowerUps will simply be ignored by the client). 31 | - Similarly, it is not necessary to check (at the contract level) whether `keyword` is formatted properly or using the correct prefixes/namespaces. 32 | - Every time a new PowerUp is created, or an existing PowerUp is 'topped up', an event should be emitted that includes the timestamp, and the new amount of tokens that have been burned in association with the TopUp (this is what will allow the client to 'score' a given PowerUp based both on the total number of tokens that have been burned and -- critically -- how long ago those tokens were burned). 33 | -------------------------------------------------------------------------------- /contracts/registry/ContractManager.sol: -------------------------------------------------------------------------------- 1 | /* solium-disable security/no-block-members */ 2 | 3 | pragma solidity 0.5.7; 4 | 5 | import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; 6 | import "openzeppelin-solidity/contracts/utils/Address.sol"; 7 | 8 | 9 | /** 10 | * @dev Contract Version Manager for non-upgradeable contracts 11 | */ 12 | contract ContractManager is Ownable { 13 | 14 | event VersionAdded( 15 | string contractName, 16 | string versionName, 17 | address indexed implementation 18 | ); 19 | 20 | event VersionUpdated( 21 | string contractName, 22 | string versionName, 23 | Status status, 24 | BugLevel bugLevel 25 | ); 26 | 27 | event VersionRecommended(string contractName, string versionName); 28 | 29 | event RecommendedVersionRemoved(string contractName); 30 | 31 | /** 32 | * @dev Signifies the status of a version 33 | */ 34 | enum Status {BETA, RC, PRODUCTION, DEPRECATED} 35 | 36 | /** 37 | * @dev Indicated the highest level of bug found in the version 38 | */ 39 | enum BugLevel {NONE, LOW, MEDIUM, HIGH, CRITICAL} 40 | 41 | /** 42 | * @dev A struct to encode version details 43 | */ 44 | struct Version { 45 | // the version number string ex. "v1.0" 46 | string versionName; 47 | 48 | Status status; 49 | 50 | BugLevel bugLevel; 51 | // the address of the instantiation of the version 52 | address implementation; 53 | // the date when this version was registered with the contract 54 | uint256 dateAdded; 55 | } 56 | 57 | /** 58 | * @dev List of all contracts registered (append-only) 59 | */ 60 | string[] internal contracts; 61 | 62 | /** 63 | * @dev Mapping to keep track which contract names have been registered. 64 | * Used to save gas when checking for redundant contract names 65 | */ 66 | mapping(string=>bool) internal contractExists; 67 | 68 | /** 69 | * @dev Mapping to keep track of all version names for easch contract name 70 | */ 71 | mapping(string=>string[]) internal contractVsVersionString; 72 | 73 | /** 74 | * @dev Mapping from contract names to version names to version structs 75 | */ 76 | mapping(string=>mapping(string=>Version)) internal contractVsVersions; 77 | 78 | /** 79 | * @dev Mapping from contract names to the version names of their 80 | * recommended versions 81 | */ 82 | mapping(string=>string) internal contractVsRecommendedVersion; 83 | 84 | modifier nonZeroAddress(address _address) { 85 | require( 86 | _address != address(0), 87 | "The provided address is the 0 address" 88 | ); 89 | _; 90 | } 91 | 92 | modifier contractRegistered(string memory contractName) { 93 | 94 | require(contractExists[contractName], "Contract does not exists"); 95 | _; 96 | } 97 | 98 | modifier versionExists( 99 | string memory contractName, 100 | string memory versionName 101 | ) 102 | { 103 | require( 104 | contractVsVersions[contractName][versionName].implementation != address(0), 105 | "Version does not exists for contract" 106 | ); 107 | _; 108 | } 109 | 110 | /** 111 | * @dev Allows owner to register a new version of a contract 112 | * @param contractName The contract name of the contract to be added 113 | * @param versionName The name of the version to be added 114 | * @param status Status of the version to be added 115 | * @param implementation The address of the implementation of the version 116 | */ 117 | function addVersion( 118 | string calldata contractName, 119 | string calldata versionName, 120 | Status status, 121 | address implementation 122 | ) 123 | external 124 | onlyOwner 125 | nonZeroAddress(implementation) 126 | { 127 | // version name must not be the empty string 128 | require(bytes(versionName).length>0, "Empty string passed as version"); 129 | 130 | // contract name must not be the empty string 131 | require( 132 | bytes(contractName).length>0, 133 | "Empty string passed as contract name" 134 | ); 135 | 136 | // implementation must be a contract 137 | require( 138 | Address.isContract(implementation), 139 | "Cannot set an implementation to a non-contract address" 140 | ); 141 | 142 | if (!contractExists[contractName]) { 143 | contracts.push(contractName); 144 | contractExists[contractName] = true; 145 | } 146 | 147 | // the version name should not already be registered for 148 | // the given contract 149 | require( 150 | contractVsVersions[contractName][versionName].implementation == address(0), 151 | "Version already exists for contract" 152 | ); 153 | contractVsVersionString[contractName].push(versionName); 154 | 155 | contractVsVersions[contractName][versionName] = Version({ 156 | versionName:versionName, 157 | status:status, 158 | bugLevel:BugLevel.NONE, 159 | implementation:implementation, 160 | dateAdded:block.timestamp 161 | }); 162 | 163 | emit VersionAdded(contractName, versionName, implementation); 164 | } 165 | 166 | /** 167 | * @dev Allows owner to update a contract version 168 | * @param contractName Name of the contract 169 | * @param versionName Version of the contract 170 | * @param status Status of the contract 171 | * @param bugLevel New bug level for the contract 172 | */ 173 | function updateVersion( 174 | string calldata contractName, 175 | string calldata versionName, 176 | Status status, 177 | BugLevel bugLevel 178 | ) 179 | external 180 | onlyOwner 181 | contractRegistered(contractName) 182 | versionExists(contractName, versionName) 183 | { 184 | 185 | contractVsVersions[contractName][versionName].status = status; 186 | contractVsVersions[contractName][versionName].bugLevel = bugLevel; 187 | 188 | emit VersionUpdated( 189 | contractName, 190 | versionName, 191 | status, 192 | bugLevel 193 | ); 194 | } 195 | 196 | /** 197 | * @dev Allows owner to set the recommended version 198 | * @param contractName Name of the contract 199 | * @param versionName Version of the contract 200 | */ 201 | function markRecommendedVersion( 202 | string calldata contractName, 203 | string calldata versionName 204 | ) 205 | external 206 | onlyOwner 207 | contractRegistered(contractName) 208 | versionExists(contractName, versionName) 209 | { 210 | // set the version name as the recommended version 211 | contractVsRecommendedVersion[contractName] = versionName; 212 | 213 | emit VersionRecommended(contractName, versionName); 214 | } 215 | 216 | /** 217 | * @dev Get recommended version for the contract. 218 | * @return Details of recommended version 219 | */ 220 | function getRecommendedVersion( 221 | string calldata contractName 222 | ) 223 | external 224 | view 225 | contractRegistered(contractName) 226 | returns ( 227 | string memory versionName, 228 | Status status, 229 | BugLevel bugLevel, 230 | address implementation, 231 | uint256 dateAdded 232 | ) 233 | { 234 | versionName = contractVsRecommendedVersion[contractName]; 235 | 236 | Version storage recommendedVersion = contractVsVersions[ 237 | contractName 238 | ][ 239 | versionName 240 | ]; 241 | 242 | status = recommendedVersion.status; 243 | bugLevel = recommendedVersion.bugLevel; 244 | implementation = recommendedVersion.implementation; 245 | dateAdded = recommendedVersion.dateAdded; 246 | 247 | return ( 248 | versionName, 249 | status, 250 | bugLevel, 251 | implementation, 252 | dateAdded 253 | ); 254 | } 255 | 256 | /** 257 | * @dev Allows owner to remove a version from being recommended 258 | * @param contractName Name of the contract 259 | */ 260 | function removeRecommendedVersion(string calldata contractName) 261 | external 262 | onlyOwner 263 | contractRegistered(contractName) 264 | { 265 | // delete the recommended version name from the mapping 266 | delete contractVsRecommendedVersion[contractName]; 267 | 268 | emit RecommendedVersionRemoved(contractName); 269 | } 270 | 271 | /** 272 | * @dev Get total count of contracts registered 273 | */ 274 | function getTotalContractCount() external view returns (uint256 count) { 275 | count = contracts.length; 276 | return count; 277 | } 278 | 279 | /** 280 | * @dev Get total count of versions for the contract 281 | * @param contractName Name of the contract 282 | */ 283 | function getVersionCountForContract(string calldata contractName) 284 | external 285 | view 286 | returns (uint256 count) 287 | { 288 | count = contractVsVersionString[contractName].length; 289 | return count; 290 | } 291 | 292 | /** 293 | * @dev Returns the contract at a given index in the contracts array 294 | * @param index The index to be searched for 295 | */ 296 | function getContractAtIndex(uint256 index) 297 | external 298 | view 299 | returns (string memory contractName) 300 | { 301 | contractName = contracts[index]; 302 | return contractName; 303 | } 304 | 305 | /** 306 | * @dev Returns the version name of a contract at specific index in a 307 | * contractVsVersionString[contractName] array 308 | * @param contractName Name of the contract 309 | * @param index The index to be searched for 310 | */ 311 | function getVersionAtIndex(string calldata contractName, uint256 index) 312 | external 313 | view 314 | returns (string memory versionName) 315 | { 316 | versionName = contractVsVersionString[contractName][index]; 317 | return versionName; 318 | } 319 | 320 | /** 321 | * @dev Returns the version details for the given contract and version name 322 | * @param contractName Name of the contract 323 | * @param versionName Version string for the contract 324 | */ 325 | function getVersionDetails( 326 | string calldata contractName, 327 | string calldata versionName 328 | ) 329 | external 330 | view 331 | returns ( 332 | string memory versionString, 333 | Status status, 334 | BugLevel bugLevel, 335 | address implementation, 336 | uint256 dateAdded 337 | ) 338 | { 339 | Version storage v = contractVsVersions[contractName][versionName]; 340 | 341 | versionString = v.versionName; 342 | status = v.status; 343 | bugLevel = v.bugLevel; 344 | implementation = v.implementation; 345 | dateAdded = v.dateAdded; 346 | 347 | return ( 348 | versionString, 349 | status, 350 | bugLevel, 351 | implementation, 352 | dateAdded 353 | ); 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /contracts/registry/ContractManagerSpec.md: -------------------------------------------------------------------------------- 1 | # ContractManager Contract Specification 2 | 3 | ## Introduction 4 | 5 | The OpenBazaar client will interact with the Escrow contract, as well as a handful of other contracts in the future (a 'utility' contract, a 'rewards' contract, a 'name server' contract, etc). As time goes by, we may want to create new versions of these contracts for the client to use. However, it is important that we do not _force_ existing clients to use our newly updated contracts (such behavior would go against OpenBazaar's core values). So rather than _altering_ existing contracts (and thereby forcing changes on any clients who are using the existing contracts), we'll instead deploy entirely new contracts and allow the clients to _opt in_ to using them. 6 | 7 | The purpose of the ContractManager is simply to provide a way for OB1 to signal to the client software which version(s) of the contracts are currently recommended (by OB1). When a new version of a contract is deployed, information about that version of the contract is added to the ContractManager contract. If an earlier version of a contract is found to have a bug, OB1 can signal that to the clients via the ContractManager. 8 | 9 | We could achieve the same thing by using signed messages stored on a server or on IPFS. However, storing this information in the Ethereum blockchain gives us very high uptime guarantees with an (amortized) cost of $0 -- all while not having to maintain any hardware or incentivize any IPFS hosts. 10 | 11 | ## Intended Functionality 12 | 13 | This contract is intended simply as a way for OB1 to communicate basic information about the state of OpenBazaar contracts to the clients. As such, only the Owner of the contract should be able to add/change/remove any information from the ContractManager. 14 | 15 | Each contract added to the ContractManager has a `contractName`, which indicates its _type_ (ie: 'escrow', 'utility', 'rewards', 'wns', etc). It has a `versionName` which indicates the _version_ (ie 'v0.0.1', 'v1.2.0', etc). It has a `status` which is one of {BETA, RC, PRODUCTION, DEPRECATED}. It has a `bugLevel` which is one of {NONE, LOW, MEDIUM, HIGH, CRITICAL}. It has an `implementation`, which is simply the address of its instantiation. It has a timestamp of when the contract was added to the registry. 16 | 17 | The Owner (and only the Owner) of the ContractManager should be able to register new contracts as well as change the `status` indicator, and `bugLevel` of registered contracts. 18 | 19 | For each `contractName`, the Owner (and only the Owner) should be able to mark up to one `versionName` as the "recommended" version for a particular `contractName`. They should also be able to _remove_ the "recommended" status of any version. 20 | -------------------------------------------------------------------------------- /contracts/rewards/OBRewards.sol: -------------------------------------------------------------------------------- 1 | /* solium-disable security/no-block-members */ 2 | 3 | pragma solidity 0.5.7; 4 | 5 | import "openzeppelin-solidity/contracts/math/SafeMath.sol"; 6 | import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; 7 | import "../token/ITokenContract.sol"; 8 | import "../escrow/IEscrow.sol"; 9 | 10 | 11 | /** 12 | * @dev This contract will distribute tokens to the buyers who purchase items from 13 | * the OB verified sellers 14 | * For more information please visit below mentioned link 15 | * https://github.com/OB1Company/openbazaar-smart-contracts/issues/10 16 | */ 17 | contract OBRewards is Ownable { 18 | 19 | using SafeMath for uint256; 20 | 21 | //Mapping of promoted sellers 22 | mapping(address => bool) public promotedSellers; 23 | 24 | uint256 public maxRewardPerSeller; 25 | 26 | uint256 public maxRewardToBuyerPerSeller; 27 | 28 | uint256 public totalTokensDistributed; 29 | 30 | //A time window in seconds where purchases between A and B will be 31 | //rewarded with tokens. Ie if a trade is completed satisfactorily at 32 | //time X, then the buyer can claim their reward any time after X and 33 | //before X + timeWindow. 34 | uint256 public timeWindow; 35 | 36 | //Mapping of seller to all buyers who received rewards by purchasing 37 | //from that seller. 38 | mapping(address => address[]) sellerVsBuyersArray; 39 | 40 | //Mapping of seller and buyer to a bool indicating whether the buyers has 41 | //claimed any rewards from that seller. 42 | mapping(address => mapping(address => bool)) sellerVsBuyersBool; 43 | 44 | //Given a seller and a buyer, this will return the amount of tokens that 45 | //have been rewarded to the buyer for purchasing from the seller. 46 | mapping(address => mapping(address => uint256)) sellerVsBuyerRewards; 47 | 48 | //For each seller, this returns the total number of tokens that have been 49 | //given out as rewards for purchasing from that seller. 50 | mapping(address => uint256) sellerVsRewardsDistributed; 51 | 52 | //Escrow contract address which will be used to calculate and validate 53 | //transactions 54 | IEscrow public escrowContract; 55 | 56 | //Address of the reward Token to be distributed to the buyers 57 | ITokenContract public obToken; 58 | 59 | //Bool to signify whether reward distribution is active or not 60 | bool public rewardsOn; 61 | 62 | //End date of the promotion 63 | uint256 public endDate; 64 | 65 | event SuccessfulClaim( 66 | bytes32 indexed scriptHash, 67 | address indexed seller, 68 | address indexed buyer, 69 | uint256 amount 70 | ); 71 | 72 | event UnsuccessfulClaim( 73 | bytes32 indexed scriptHash, 74 | address indexed seller, 75 | address indexed buyer 76 | ); 77 | 78 | event PromotedSellersAdded(address[] seller); 79 | 80 | event PromotedSellersRemoved(address[] seller); 81 | 82 | event RewardsOn(); 83 | 84 | event EndDateChanged(uint256 endDate); 85 | 86 | modifier nonZeroAddress(address _address) { 87 | require(_address != address(0), "Zero address not allowed"); 88 | _; 89 | } 90 | 91 | modifier rewardsRunning() { 92 | require( 93 | rewardsOn && (endDate > block.timestamp), 94 | "Reward distribution is not running" 95 | ); 96 | _; 97 | } 98 | 99 | /** 100 | * @dev Add details to rewards contract at the time of deployment 101 | * @param _maxRewardPerSeller Maximum reward to be distributed from 102 | * each seller 103 | * @param _timeWindow A time window, in seconds, where purchases 104 | * will be rewarded with tokens 105 | * @param _escrowContractAddress Escrow address to be considered for 106 | * rewards distribution. 107 | * @param obTokenAddress Address of the reward token 108 | */ 109 | constructor( 110 | uint256 _maxRewardPerSeller, 111 | uint256 _timeWindow, 112 | address _escrowContractAddress, // this should be a trusted contract 113 | address obTokenAddress 114 | ) 115 | public 116 | nonZeroAddress(_escrowContractAddress) 117 | nonZeroAddress(obTokenAddress) 118 | { 119 | 120 | require( 121 | _maxRewardPerSeller > 0, 122 | "Maximum reward must be greater than 0" 123 | ); 124 | 125 | require( 126 | _timeWindow > 0, 127 | "Time window must be greater than 0" 128 | ); 129 | 130 | maxRewardPerSeller = _maxRewardPerSeller; 131 | timeWindow = _timeWindow; 132 | escrowContract = IEscrow(_escrowContractAddress); 133 | obToken = ITokenContract(obTokenAddress); 134 | maxRewardToBuyerPerSeller = uint256(50).mul( 135 | 10 ** uint256(obToken.decimals()) 136 | ); 137 | 138 | } 139 | 140 | /** 141 | * @dev Allows owner to add new promoted sellers. Previous ones will 142 | * remain untouched 143 | * @param sellers List of sellers to be marked as promoted 144 | * No Seller out of this list should already be promoted, otherwise 145 | * transaction will fail 146 | */ 147 | function addPromotedSellers( 148 | address[] calldata sellers 149 | ) 150 | external 151 | onlyOwner 152 | { 153 | 154 | for (uint256 i = 0; i < sellers.length; i++) { 155 | require( 156 | sellers[i] != address(0), 157 | "Zero address cannot be a promoted seller" 158 | ); 159 | 160 | require( 161 | !promotedSellers[sellers[i]], 162 | "One of the sellers is already promoted" 163 | ); //Also protects against the same being address passed twice. 164 | 165 | promotedSellers[sellers[i]] = true; 166 | } 167 | emit PromotedSellersAdded(sellers); 168 | } 169 | 170 | /** 171 | * @dev Remove exisiting promoted sellers 172 | * @param sellers List of sellers to be removed 173 | */ 174 | function removePromotedSellers( 175 | address[] calldata sellers 176 | ) 177 | external 178 | onlyOwner 179 | { 180 | 181 | for (uint256 i = 0; i < sellers.length; i++) { 182 | 183 | promotedSellers[sellers[i]] = false; 184 | } 185 | emit PromotedSellersRemoved(sellers); 186 | } 187 | 188 | /** 189 | * @dev Returns list of buyers that have been rewarded for purchasing from 190 | * a given seller 191 | * @param seller Address of promoted seller 192 | * @return buyers List of Buyers 193 | */ 194 | function getRewardedBuyers( 195 | address seller 196 | ) 197 | external 198 | view 199 | returns (address[] memory buyers) 200 | { 201 | buyers = sellerVsBuyersArray[seller]; 202 | return buyers; 203 | } 204 | 205 | /** 206 | * @dev Return reward info for a buyer against a promoted seller 207 | * @param seller Address of promoted seller 208 | * @param buyer The buyer who reward info has to be fetched 209 | * @return rewardAmount 210 | */ 211 | function getBuyerRewardInfo( 212 | address seller, 213 | address buyer 214 | ) 215 | external 216 | view 217 | returns( 218 | uint256 rewardAmount 219 | ) 220 | { 221 | return sellerVsBuyerRewards[seller][buyer]; 222 | } 223 | 224 | /** 225 | * @dev Total reward distributed for a promoted seller so far 226 | * @param seller Promoted seller's address 227 | * @return Amount of tokens distributed as reward for a seller 228 | */ 229 | function getDistributedReward( 230 | address seller 231 | ) 232 | external 233 | view 234 | returns (uint256 rewardDistributed) 235 | { 236 | rewardDistributed = sellerVsRewardsDistributed[seller]; 237 | return rewardDistributed; 238 | } 239 | 240 | /** 241 | * @dev Allows the owner of the contract to transfer all remaining tokens to 242 | * an address of their choosing. 243 | * @param receiver The receiver's address 244 | */ 245 | function transferRemainingTokens( 246 | address receiver 247 | ) 248 | external 249 | onlyOwner 250 | nonZeroAddress(receiver) 251 | { 252 | uint256 amount = obToken.balanceOf(address(this)); 253 | 254 | obToken.transfer(receiver, amount); 255 | } 256 | 257 | /** 258 | * @dev Method to allow the owner to adjust the maximum reward per seller 259 | * @param _maxRewardPerSeller Max reward to be distributed for each seller 260 | */ 261 | function changeMaxRewardPerSeller( 262 | uint256 _maxRewardPerSeller 263 | ) 264 | external 265 | onlyOwner 266 | { 267 | maxRewardPerSeller = _maxRewardPerSeller; 268 | } 269 | 270 | /** 271 | * @dev Method to allow the owner to change the timeWindow variable 272 | * @param _timeWindow A time window in seconds 273 | */ 274 | function changeTimeWindow(uint256 _timeWindow) external onlyOwner { 275 | timeWindow = _timeWindow; 276 | } 277 | 278 | /** 279 | * @dev Returns the number of rewarded buyers associated with a given seller 280 | * @param seller Address of the promoted seller 281 | */ 282 | function noOfRewardedBuyers( 283 | address seller 284 | ) 285 | external 286 | view 287 | returns (uint256 size) 288 | { 289 | size = sellerVsBuyersArray[seller].length; 290 | return size; 291 | } 292 | 293 | /** 294 | * @dev Method to get rewarded buyer address at specific index for a seller 295 | * @param seller Seller for whom the rewarded buyer is requested 296 | * @param index Index at which buyer has to be retrieved 297 | */ 298 | function getRewardedBuyer( 299 | address seller, 300 | uint256 index 301 | ) 302 | external 303 | view 304 | returns (address buyer) 305 | { 306 | require( 307 | sellerVsBuyersArray[seller].length > index, 308 | "Array index out of bound" 309 | ); 310 | buyer = sellerVsBuyersArray[seller][index]; 311 | return buyer; 312 | } 313 | 314 | /** 315 | * @dev Allows the owner of the contract to turn on the rewards distribution 316 | * Only if it was not previously turned on 317 | */ 318 | function turnOnRewards() external onlyOwner { 319 | require(!rewardsOn, "Rewards distribution is already on"); 320 | 321 | rewardsOn = true; 322 | 323 | emit RewardsOn(); 324 | } 325 | 326 | /** 327 | * @dev ALlows owner to set endDate 328 | * @param _endDate date the promotion ends 329 | */ 330 | function setEndDate(uint256 _endDate) external onlyOwner { 331 | require( 332 | _endDate > block.timestamp, 333 | "End date should be greater than current date" 334 | ); 335 | 336 | endDate = _endDate; 337 | 338 | emit EndDateChanged(endDate); 339 | } 340 | 341 | function isRewardsRunning() external view returns (bool running) { 342 | running = rewardsOn && (endDate > block.timestamp); 343 | return running; 344 | } 345 | 346 | /** 347 | * @dev Buyer can call this method to calculate the reward for their 348 | * transaction 349 | * @param scriptHash Script hash of the transaction 350 | */ 351 | function calculateReward( 352 | bytes32 scriptHash 353 | ) 354 | public 355 | view 356 | returns (uint256 amount) 357 | { 358 | ( 359 | address buyer, 360 | address seller, 361 | uint8 status, 362 | uint256 lastModified 363 | ) = _getTransaction(scriptHash); 364 | 365 | amount = _getTokensToReward( 366 | scriptHash, 367 | buyer, 368 | seller, 369 | status, 370 | lastModified 371 | ); 372 | 373 | return amount; 374 | } 375 | 376 | /** 377 | * @dev Using this method user can choose to execute their transaction and 378 | * claim their rewards in one go. This will save one transaction. 379 | * Users can only use this method if their trade is using escrowContract 380 | * for escrow. 381 | * See the execute() method Escrow.sol for more information. 382 | */ 383 | function executeAndClaim( 384 | uint8[] memory sigV, 385 | bytes32[] memory sigR, 386 | bytes32[] memory sigS, 387 | bytes32 scriptHash, 388 | address[] memory destinations, 389 | uint256[] memory amounts 390 | ) 391 | public 392 | rewardsRunning 393 | 394 | { 395 | //1. Execute transaction 396 | //SECURITY NOTE: `escrowContract` is a known and trusted contract, but 397 | //the `execute` function transfers ETH or Tokens, and therefore hands 398 | //over control of the logic flow to a potential attacker. 399 | escrowContract.execute( 400 | sigV, 401 | sigR, 402 | sigS, 403 | scriptHash, 404 | destinations, 405 | amounts 406 | ); 407 | 408 | //2. Claim Reward 409 | bytes32[] memory scriptHashes = new bytes32[](1); 410 | scriptHashes[0] = scriptHash; 411 | 412 | claimRewards(scriptHashes); 413 | } 414 | 415 | /** 416 | * @dev Function to claim tokens 417 | * @param scriptHashes Array of scriptHashes of OB trades for which 418 | * the buyer wants to claim reward tokens. 419 | * Note that a Buyer can perform trades with multiple promoted sellers and 420 | * then can claim their reward tokens all at once for all those trades using 421 | * this function. 422 | * Be mindful of the block gas limit (do not pass too many scripthashes). 423 | */ 424 | function claimRewards( 425 | bytes32[] memory scriptHashes 426 | ) 427 | public 428 | rewardsRunning 429 | { 430 | 431 | require(scriptHashes.length > 0, "No script hash passed"); 432 | 433 | for (uint256 i = 0; i < scriptHashes.length; i++) { 434 | 435 | //1. Get the transaction from Escrow contract 436 | ( 437 | address buyer, 438 | address seller, 439 | uint8 status, 440 | uint256 lastModified 441 | ) = _getTransaction(scriptHashes[i]); 442 | 443 | //2. Check that the transaction exists 444 | //3. Check seller is promoted seller and the 445 | //timeWindow has not closed 446 | //4. Get the number of tokens to be given as reward 447 | //5. The seller must be one of the beneficiaries 448 | uint256 rewardAmount = _getTokensToReward( 449 | scriptHashes[i], 450 | buyer, 451 | seller, 452 | status, 453 | lastModified 454 | ); 455 | 456 | uint256 contractBalance = obToken.balanceOf(address(this)); 457 | 458 | if (rewardAmount > contractBalance) { 459 | rewardAmount = contractBalance; 460 | } 461 | 462 | if (rewardAmount == 0) { 463 | emit UnsuccessfulClaim(scriptHashes[i], seller, buyer); 464 | continue; 465 | } 466 | 467 | //6. Update state 468 | if (!sellerVsBuyersBool[seller][buyer]) { 469 | sellerVsBuyersBool[seller][buyer] = true; 470 | sellerVsBuyersArray[seller].push(buyer); 471 | } 472 | 473 | sellerVsBuyerRewards[seller][buyer] = sellerVsBuyerRewards[ 474 | seller 475 | ][ 476 | buyer 477 | ].add(rewardAmount); 478 | 479 | sellerVsRewardsDistributed[seller] = sellerVsRewardsDistributed[ 480 | seller 481 | ].add(rewardAmount); 482 | 483 | totalTokensDistributed = totalTokensDistributed.add(rewardAmount); 484 | 485 | //7. Emit event 486 | emit SuccessfulClaim( 487 | scriptHashes[i], 488 | seller, 489 | buyer, 490 | rewardAmount 491 | ); 492 | 493 | //8. Transfer token 494 | obToken.transfer(buyer, rewardAmount); 495 | } 496 | 497 | } 498 | 499 | //Private method to get transaction info out from the escrow contract 500 | function _getTransaction( 501 | bytes32 _scriptHash 502 | ) 503 | private 504 | view 505 | returns( 506 | address buyer, 507 | address seller, 508 | uint8 status, 509 | uint256 lastModified 510 | ) 511 | { 512 | // calling a trusted contract's view function 513 | ( 514 | , 515 | lastModified, 516 | status, 517 | ,,, 518 | buyer, 519 | seller, 520 | 521 | ) = escrowContract.transactions(_scriptHash); 522 | 523 | return (buyer, seller, status, lastModified); 524 | } 525 | 526 | /** 527 | * @dev Checks -: 528 | * 1. If transaction exists 529 | * 2. If seller is promoted 530 | * 3. Transaction has been closed/released 531 | * 4. Transaction happened with the time window. 532 | * 5. Seller must be one of the beneficiaries of the transaction execution 533 | * @param scriptHash Script hash of the transaction 534 | * @param buyer Buyer in the transaction 535 | * @param seller Seller in the transaction 536 | * @param status Status of the transaction 537 | * @param lastModified Last modified time of the transaction 538 | * @return bool Returns whether transaction is valid and eligible 539 | * for rewards 540 | */ 541 | function _verifyTransactionData( 542 | bytes32 scriptHash, 543 | address buyer, 544 | address seller, 545 | uint8 status, 546 | uint256 lastModified 547 | ) 548 | private 549 | view 550 | returns (bool verified) 551 | { 552 | 553 | verified = true; 554 | 555 | if (buyer == address(0)) { 556 | //If buyer is a zero address then we treat the transaction as 557 | //a not verified 558 | verified = false; 559 | } 560 | 561 | else if (!promotedSellers[seller]) { 562 | //seller of the transaction is not a promoted seller 563 | verified = false; 564 | } 565 | else if (status != 1) { 566 | //Transaction has not been released 567 | verified = false; 568 | } 569 | else if ( 570 | //Calling a trusted contract's view function 571 | !escrowContract.checkVote(scriptHash, seller) 572 | ) 573 | { 574 | //Seller was not one of the signers 575 | verified = false; 576 | } 577 | else if ( 578 | //Calling a trusted contract's view function 579 | !escrowContract.checkBeneficiary(scriptHash, seller) 580 | ) { 581 | //Seller was not one of the beneficiaries. 582 | //This means the transaction was either cancelled or 583 | //completely refunded. 584 | verified = false; 585 | } 586 | else if (lastModified.add(timeWindow) < block.timestamp) { 587 | //timeWindow has been exceeded 588 | verified = false; 589 | } 590 | 591 | return verified; 592 | } 593 | 594 | /** 595 | * @dev Private function to get Tokens to be distributed as reward 596 | * Checks whether transaction is verified or not and computes the 597 | * amount of the rewards using the _calculateReward() method 598 | */ 599 | function _getTokensToReward( 600 | bytes32 scriptHash, 601 | address buyer, 602 | address seller, 603 | uint8 status, 604 | uint256 lastModified 605 | ) 606 | private 607 | view 608 | returns (uint256 amount) 609 | { 610 | 611 | if ( 612 | !_verifyTransactionData( 613 | scriptHash, 614 | buyer, 615 | seller, 616 | status, 617 | lastModified 618 | ) 619 | ) 620 | { 621 | amount = 0; 622 | } 623 | 624 | else { 625 | amount = _calculateReward(buyer, seller); 626 | } 627 | 628 | return amount; 629 | } 630 | 631 | /** 632 | * @dev Private function to calculate reward. 633 | * Please see link for rewards calculation algo 634 | * https://github.com/OB1Company/openbazaar-smart-contracts/issues/10 635 | */ 636 | function _calculateReward( 637 | address buyer, 638 | address seller 639 | ) 640 | private 641 | view 642 | returns (uint256 amount) 643 | { 644 | 645 | if (sellerVsRewardsDistributed[seller] >= maxRewardPerSeller) { 646 | //No more rewards can be distributed for buying from the 647 | //given seller 648 | amount = 0; 649 | } 650 | 651 | else { 652 | //maxRewardToBuyerPerSeller tokens will be given to each buyer per 653 | //seller until the maximum amount of rewards for the seller has 654 | //been exhausted 655 | amount = maxRewardToBuyerPerSeller.sub(sellerVsBuyerRewards[seller][buyer]); 656 | 657 | //Check that we are not disbursing more rewards than are 658 | //allocated per seller 659 | if (sellerVsRewardsDistributed[seller].add(amount) > maxRewardPerSeller) { 660 | amount = maxRewardPerSeller.sub(sellerVsRewardsDistributed[seller]); 661 | } 662 | 663 | } 664 | 665 | return amount; 666 | 667 | } 668 | 669 | 670 | } 671 | -------------------------------------------------------------------------------- /contracts/rewards/RewardsSpec.md: -------------------------------------------------------------------------------- 1 | # Rewards Contract Specification 2 | 3 | ## Introduction 4 | 5 | OB1 will occasionally hold 'promotions' where users who buy goods from "promoted sellers" become eligible for reward tokens (OBT). Each of these promotions will be managed by an instance of the OBRewards contract. 6 | 7 | ## Time Limits 8 | 9 | When a buyer purchases from a promoted seller they become eligible to receive up to 50 OBT from the rewards contract. The buyer has a fixed amount of time (`timeWindow` seconds) after the completion of the sale to claim their reward tokens from the contract. 10 | 11 | The promotion as a whole has an `endDate`, which is set (and changeable) by the owner. After the promotion's `endDate` has come to pass, buyers can no longer claim any rewards. 12 | 13 | ## Claiming Rewards 14 | 15 | The buyer can claim tokens for which she is eligible in on of two ways: 16 | 17 | 1. By calling `claimRewards`, the buyer can pass a list of OB transactions (identified by their `scriptHash`). The contract ensures that each of the passed transactions do indeed make the buyer eligible for some reward tokens, computes the total amount of tokens the buyer is eligible to receive, and sends that amount of reward tokens to the buyer. (If the contract does not have enough reward tokens remaining, it will send the buyer all of the tokens it has. Then, if OB1 sends more reward tokens to the contract, the buyer should be able to claim whatever remaining tokens they are owed -- assuming they are still eligible to receive the tokens.) 18 | 19 | 2. By calling `executeAndClaim` the buyer can complete their trade with the seller and claim any rewards with a single transaction. 20 | 21 | ## Limits on Reward Amounts 22 | 23 | Each buyer may be rewarded up to 50 reward tokens for purchasing from a given promoted seller. That is, if buyer Bob buys from promoted seller Sally, he'll be eligible for up to 50 reward tokens, but if he buys from her again during the same promotion, he will not be eligible for an additional 50 reward tokens. If Bob wants to earn more tokens during the same promotion, he'd have to complete a purchase from some other promoted seller. 24 | 25 | Additionally, the owner of the contract sets a maximum total number of tokens that can be rewarded for purchasing from any given promoted seller (`maxRewardPerSeller`). For example, suppose `maxRewardPerSeller` is 500 OBT and that each buyer is eligible to receive up to 50 OBT for purchasing from a given promoted seller. Then if Alice is a promoted seller, at most 10 buyers can receive rewards from purchasing from Alice. 26 | 27 | ## Additional Notes 28 | 29 | - Any reward tokens remaining in the contract can be withdrawn from the contract by the owner. 30 | 31 | - This approach to promoting sellers is subject to trivially-executable sybil attacks, as well as buyer collusion with promoted sellers. The limits on the rewards and the restriction to OB1-promoted sellers are intended to mitigate this risk. 32 | 33 | - If a buyer is eligible to receive more OBT than remains in the contract balance, then when the buyer attempts to claim their reward, the contract should pay out as much OBT as it can to the buyer. If the contract later receives more OBT, then the buyer should be able to claim the remainder of their reward. 34 | -------------------------------------------------------------------------------- /contracts/test/TestToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.5.7; 2 | 3 | import "openzeppelin-solidity/contracts/math/SafeMath.sol"; 4 | /** 5 | * @title ERC20Basic 6 | * @dev Simpler version of ERC20 interface 7 | * @dev see https://github.com/ethereum/EIPs/issues/179 8 | */ 9 | contract ERC20Basic { 10 | // Total amount of tokens 11 | uint256 public totalSupply; 12 | 13 | function balanceOf(address _owner) public view returns (uint256 balance); 14 | 15 | function transfer(address _to, uint256 _amount) public returns (bool success); 16 | 17 | event Transfer(address indexed from, address indexed to, uint256 value); 18 | } 19 | 20 | /** 21 | * @title ERC20 interface 22 | * @dev see https://github.com/ethereum/EIPs/issues/20 23 | */ 24 | contract ERC20 is ERC20Basic { 25 | function allowance(address _owner, address _spender) public view returns (uint256 remaining); 26 | 27 | function transferFrom(address _from, address _to, uint256 _amount) public returns (bool success); 28 | 29 | function approve(address _spender, uint256 _amount) public returns (bool success); 30 | 31 | event Approval(address indexed owner, address indexed spender, uint256 value); 32 | } 33 | 34 | /** 35 | * @title Basic token 36 | * @dev Basic version of StandardToken, with no allowances. 37 | */ 38 | contract BasicToken is ERC20Basic { 39 | using SafeMath for uint256; 40 | 41 | // balance in each address account 42 | mapping(address => uint256) balances; 43 | 44 | /** 45 | * @dev transfer token for a specified address 46 | * @param _to The address to transfer to. 47 | * @param _amount The amount to be transferred. 48 | */ 49 | function transfer(address _to, uint256 _amount) public returns (bool success) { 50 | require(_to != address(0)); 51 | require(balances[msg.sender] >= _amount && _amount > 0 && balances[_to].add(_amount) > balances[_to]); 52 | 53 | // SafeMath.sub will throw if there is not enough balance. 54 | balances[msg.sender] = balances[msg.sender].sub(_amount); 55 | balances[_to] = balances[_to].add(_amount); 56 | emit Transfer(msg.sender, _to, _amount); 57 | return true; 58 | } 59 | 60 | /** 61 | * @dev Gets the balance of the specified address. 62 | * @param _owner The address to query the the balance of. 63 | * @return An uint256 representing the amount owned by the passed address. 64 | */ 65 | function balanceOf(address _owner) public view returns (uint256 balance) { 66 | return balances[_owner]; 67 | } 68 | 69 | } 70 | 71 | /** 72 | * @title Standard ERC20 token 73 | * 74 | * @dev Implementation of the basic standard token. 75 | * @dev https://github.com/ethereum/EIPs/issues/20 76 | */ 77 | contract StandardToken is ERC20, BasicToken { 78 | 79 | 80 | mapping (address => mapping (address => uint256)) internal allowed; 81 | 82 | 83 | /** 84 | * @dev Transfer tokens from one address to another 85 | * @param _from address The address which you want to send tokens from 86 | * @param _to address The address which you want to transfer to 87 | * @param _amount uint256 the amount of tokens to be transferred 88 | */ 89 | function transferFrom(address _from, address _to, uint256 _amount) public returns (bool success) { 90 | require(_to != address(0)); 91 | require(balances[_from] >= _amount); 92 | require(allowed[_from][msg.sender] >= _amount); 93 | require(_amount > 0 && balances[_to].add(_amount) > balances[_to]); 94 | 95 | balances[_from] = balances[_from].sub(_amount); 96 | balances[_to] = balances[_to].add(_amount); 97 | allowed[_from][msg.sender] = allowed[_from][msg.sender].sub(_amount); 98 | emit Transfer(_from, _to, _amount); 99 | return true; 100 | } 101 | 102 | /** 103 | * @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender. 104 | * 105 | * Beware that changing an allowance with this method brings the risk that someone may use both the old 106 | * and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this 107 | * race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards: 108 | * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 109 | * @param _spender The address which will spend the funds. 110 | * @param _amount The amount of tokens to be spent. 111 | */ 112 | function approve(address _spender, uint256 _amount) public returns (bool success) { 113 | allowed[msg.sender][_spender] = _amount; 114 | emit Approval(msg.sender, _spender, _amount); 115 | return true; 116 | } 117 | 118 | /** 119 | * @dev Function to check the amount of tokens that an owner allowed to a spender. 120 | * @param _owner address The address which owns the funds. 121 | * @param _spender address The address which will spend the funds. 122 | * @return A uint256 specifying the amount of tokens still available for the spender. 123 | */ 124 | function allowance(address _owner, address _spender) public view returns (uint256 remaining) { 125 | return allowed[_owner][_spender]; 126 | } 127 | 128 | } 129 | /** 130 | * @title Test Token 131 | */ 132 | contract TestToken is StandardToken { 133 | string public name ; 134 | string public symbol ; 135 | uint8 public decimals = 18 ; 136 | 137 | 138 | /** 139 | * @dev Constructor function to initialize the initial supply of token to the creator of the contract 140 | * @param initialSupply The initial supply of tokens which will be fixed through out 141 | * @param tokenName The name of the token 142 | * @param tokenSymbol The symboll of the token 143 | */ 144 | constructor( 145 | uint256 initialSupply, 146 | string memory tokenName, 147 | string memory tokenSymbol) public { 148 | totalSupply = initialSupply.mul( 10 ** uint256(decimals)); //Update total supply with the decimal amount 149 | name = tokenName; 150 | symbol = tokenSymbol; 151 | balances[msg.sender] = totalSupply; 152 | 153 | // Emitting transfer event since assigning all tokens to the creator also corresponds to the transfer of tokens to the creator 154 | emit Transfer(address(0), msg.sender, totalSupply); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /contracts/token/ITokenContract.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.5.7; 2 | 3 | interface ITokenContract { 4 | function balanceOf(address _owner) external view returns (uint256 balance); 5 | 6 | function transfer( 7 | address _to, 8 | uint256 _amount 9 | ) 10 | external 11 | returns (bool success); 12 | 13 | function transferFrom( 14 | address _from, 15 | address _to, 16 | uint256 _amount 17 | ) 18 | external 19 | returns (bool success); 20 | 21 | function burnFrom(address from, uint256 _amount) external; 22 | 23 | function approve(address spender, uint256 value) external returns (bool); 24 | 25 | function decimals() external view returns (uint8); 26 | } 27 | -------------------------------------------------------------------------------- /contracts/token/OBToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.5.7; 2 | 3 | import "openzeppelin-solidity/contracts/token/ERC20/ERC20Burnable.sol"; 4 | 5 | 6 | contract OBToken is ERC20Burnable { 7 | 8 | string public name; 9 | string public symbol; 10 | uint8 public decimals; 11 | 12 | constructor( 13 | string memory _name, 14 | string memory _symbol, 15 | uint8 _decimals, 16 | uint256 _totalSupply 17 | ) 18 | public 19 | { 20 | 21 | name = _name; 22 | symbol = _symbol; 23 | decimals = _decimals; 24 | _mint(msg.sender, _totalSupply * (10 ** uint256(decimals))); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | var Migrations = artifacts.require("./Migrations.sol"); 2 | 3 | module.exports = function(deployer) { 4 | deployer.deploy(Migrations); 5 | }; 6 | -------------------------------------------------------------------------------- /migrations/escrow/2_Escrow_migration.js: -------------------------------------------------------------------------------- 1 | var Escrow = artifacts.require("Escrow"); 2 | 3 | module.exports = async(deployer) =>{ 4 | await deployer.deploy(Escrow); 5 | 6 | }; -------------------------------------------------------------------------------- /migrations/escrow/7_EscrowProxy_migration.js: -------------------------------------------------------------------------------- 1 | var Escrow = artifacts.require("Escrow"); 2 | var EscrowProxy = artifacts.require("EscrowProxy"); 3 | 4 | module.exports = async(deployer) =>{ 5 | await deployer.deploy(EscrowProxy, Escrow.address); 6 | 7 | }; -------------------------------------------------------------------------------- /migrations/powerUps/6_Powered_Ups_migration.js: -------------------------------------------------------------------------------- 1 | var OBToken = artifacts.require("OBToken"); 2 | var PowerUps = artifacts.require("PowerUps"); 3 | 4 | 5 | module.exports = function(deployer) { 6 | deployer.deploy(PowerUps, OBToken.address); 7 | }; -------------------------------------------------------------------------------- /migrations/registry/3_contract_manager_migration.js: -------------------------------------------------------------------------------- 1 | var ContractManager = artifacts.require("ContractManager"); 2 | 3 | module.exports = function(deployer) { 4 | deployer.deploy(ContractManager); 5 | }; -------------------------------------------------------------------------------- /migrations/rewards/5_Rewards_Migration.js: -------------------------------------------------------------------------------- 1 | var OBRewards = artifacts.require("OBRewards"); 2 | var Escrow = artifacts.require("Escrow"); 3 | var OBToken = artifacts.require("OBToken"); 4 | 5 | module.exports = function(deployer) { 6 | //This is dummy data 7 | deployer.deploy( 8 | OBRewards, 9 | "50000000000000000000", 10 | 432000, 11 | Escrow.address, 12 | OBToken.address 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /migrations/token/4_OB_Token_migration.js: -------------------------------------------------------------------------------- 1 | var OBToken = artifacts.require("OBToken"); 2 | 3 | module.exports = function(deployer) { 4 | deployer.deploy(OBToken, "Open Bazaar", "OBT", 18, 1000000000); 5 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "smart-contracts", 3 | "version": "1.0.0", 4 | "description": "This repository contains all open bazaar smart contracts ## Getting Started", 5 | "main": "truffle.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "devDependencies": { 10 | "babel-polyfill": "^6.26.0", 11 | "babel-preset-es2015": "^6.18.0", 12 | "babel-preset-stage-2": "^6.18.0", 13 | "babel-preset-stage-3": "^6.17.0", 14 | "babel-register": "^6.26.0", 15 | "bignumber.js": "^7.2.1", 16 | "child_process": "^1.0.2", 17 | "coveralls": "^3.0.2", 18 | "dotenv": "^6.0.0", 19 | "eth-lightwallet": "^3.0.1", 20 | "ethereumjs-util": "^5.2.0", 21 | "ethlint": "^1.2.4", 22 | "fs": "0.0.1-security", 23 | "ganache-cli": "^6.1.4", 24 | "ganache-cli-coverage": "github:Agusx1211/ganache-cli#c462b3fc48fe9b16756f7799885c0741114d9ed3", 25 | "husky": "^1.1.0", 26 | "left-pad": "^1.3.0", 27 | "lodash": "^4.17.11", 28 | "openzeppelin-solidity": "2.1.2", 29 | "path": "^0.12.7", 30 | "ripemd160": "^2.0.2", 31 | "secp256k1": "^3.5.0", 32 | "solidity-coverage": "github:rotcivegaf/solidity-coverage#5875f5b7bc74d447f3312c9c0e9fc7814b482477", 33 | "truffle": "5.0.12", 34 | "web3": "^1.0.0-beta.34", 35 | "websocket": "^1.0.28", 36 | "yargs": "^11.0.0" 37 | }, 38 | "scripts": { 39 | "test": "scripts/test.sh", 40 | "compile": "truffle compile", 41 | "migrate": "truffle migrate", 42 | "networks": "truffle networks", 43 | "coverage": "scripts/coverage.sh", 44 | "lint:sol": "solium -d .", 45 | "lint:sol:fix": "solium -d . --fix" 46 | }, 47 | "husky": { 48 | "hooks": { 49 | "pre-commit": "truffle compile && npm run lint:sol:fix" 50 | } 51 | }, 52 | "repository": { 53 | "type": "git", 54 | "url": "git+https://github.com/OpenBazaar/smart-contracts.git" 55 | }, 56 | "keywords": [], 57 | "author": "Sameep Singhania", 58 | "license": "MIT", 59 | "bugs": { 60 | "url": "https://github.com/OpenBazaar/smart-contracts.git/issues" 61 | }, 62 | "homepage": "https://github.com/OpenBazaar/smart-contracts.git#readme" 63 | } 64 | -------------------------------------------------------------------------------- /scripts/coverage.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | SOLIDITY_COVERAGE=true scripts/test.sh -------------------------------------------------------------------------------- /scripts/signing.js: -------------------------------------------------------------------------------- 1 | const secp256k1 = require('secp256k1'); 2 | const util = require("ethereumjs-util"); 3 | const Web3 = require('web3'); 4 | var leftPad = require('left-pad'); 5 | const yargs = require("yargs"); 6 | var lightwallet = require('eth-lightwallet') 7 | var RIPEMD160 = require('ripemd160'); 8 | 9 | var argv = yargs.usage('Usage: $0 [options]') 10 | .command('generateHash', 'Generates hash for the data') 11 | .command('generatePersonalMessageHash','Generates Ethereum personal hash out of message hash') 12 | .command("signMessageUsingPrivateKey", "Signs using using private key provided") 13 | .command("signMessageUsingLocalWeb3", "Signs Message using local web3") 14 | .command("preimageHash", "Generate hash for preimage") 15 | .command("splitPayments", "Get data for splitpayments") 16 | .command("generateMUltiHash", "Generates multi hash for the data") 17 | .command("generateRedeemScript", "Generates redeem script") 18 | .command("getScriptHash", "Generates redeem script hash") 19 | .command("getUniqueId", "Generates unique Id") 20 | .command("getScriptHashFromData", "Generates Script Hash from Data") 21 | .example('$0 generateHash -t 24 -m 0x546ba5c6abc490e66cb25a081868822c1ceb3abd -da 0x61adaf40a389761bacf76dfccf682e9200989894 -d 0x -v 10000000000', 'Generate hash for the data') 22 | .alias('m', 'multisig') 23 | .alias('t', "txId") 24 | .alias('r', 'destination') 25 | .alias('d', 'data') 26 | .alias('v', 'value') 27 | .alias('p', 'privateKey') 28 | .alias('H', 'msgHash') 29 | .alias('e','personalHash') 30 | .alias('s','signer') 31 | .alias('i', 'preimage') 32 | .alias("b", "buyer") 33 | .alias("S","seller") 34 | .alias("p", "buyerPercentage") 35 | .alias("P","sellerPercentage") 36 | .alias("u", "uniqueId") 37 | .alias("T", "threshold") 38 | .alias("l", "timeoutHours") 39 | .alias("M","moderator") 40 | .alias("R", "redeemScript") 41 | .alias("k", "scriptHash") 42 | .option('destination', { 43 | type: 'array', 44 | desc: 'destination address' 45 | }) 46 | .option('value', { 47 | type: 'array', 48 | desc: 'Value to be sent to addresses' 49 | }) 50 | .number('t') 51 | .describe('m', 'Address of multisig wallet') 52 | .describe('t', "Transaction Id") 53 | .describe('r', "Destination Address") 54 | .describe('d','meta data') 55 | .describe('v', 'Value of transaction in wei') 56 | .describe('H',"Message Hash") 57 | .describe("p", "Private Key to sign transaction") 58 | .describe("e", "Personal Hash") 59 | .describe('s','Address(moderator) used to sign transaction in case of web3') 60 | .describe('i', 'Preimage whose keccak256 hash has to be calculated') 61 | .describe("b","Buyer in the transaction") 62 | .describe("S", "seller in the transaction") 63 | .describe("p","Percentage of transaction value assigned to buyer in split payments") 64 | .describe("u","Unique Id") 65 | .describe("T", "Threshold") 66 | .describe("l","Timeout hours") 67 | .describe("M","Address of the moderator") 68 | .describe("R", "Redeem Script") 69 | .describe("k", "Script hash") 70 | .help('h') 71 | .alias('h', 'help') 72 | .argv; 73 | 74 | var command = argv._[0]; 75 | 76 | const generatePreimageHash = (preimage)=>{ 77 | return "0x" + util.keccak256(preimage).toString("hex"); 78 | } 79 | 80 | const createMsgHash = (multisigAddr, transactionId, destinationAddr, value, data)=>{ 81 | let input = '0x19' + '00' + multisigAddr.slice(2) + destinationAddr[0].slice(2) + leftPad(Number(value[0]).toString('16'), '64', '0') + data.slice(2) + leftPad(transactionId.toString('16'), '64', '0') 82 | let hash = util.keccak256(input); 83 | return "0x" + hash.toString("hex"); 84 | } 85 | const generateRedeemScript = (uniqueId, threshold, timeoutHours, buyer, seller, moderator)=>{ 86 | let redeemScript = uniqueId + leftPad(threshold.toString(16), '2', '0') + leftPad(timeoutHours.toString(16), '8', '0') + buyer.slice(2) + seller.slice(2) + moderator.slice(2); 87 | return redeemScript.toString('hex'); 88 | } 89 | 90 | const getScriptHashFromData = (uniqueId, threshold, timeoutHours, buyer, seller, moderator) => { 91 | var redeeemScript = generateRedeemScript(uniqueId, threshold, timeoutHours, buyer, seller, moderator); 92 | let hash = getScriptHashFromData(redeeemScript); 93 | 94 | return hash; 95 | } 96 | 97 | const getScriptHash = (redeemScript) => { 98 | let hash = util.keccak256(redeemScript); 99 | return "0x" + hash.toString('hex'); 100 | } 101 | 102 | const createMultiMsgHash = (multisigAddr, scriptHash, destinationAddrs, values)=>{ 103 | //console.log(destinationAddrs, values) 104 | var dest= ""; 105 | var vals="" ; 106 | for(var i = 0;i{ 119 | let hash = util.hashPersonalMessage(util.toBuffer(msgHash)); 120 | 121 | return hash.toString("hex"); 122 | } 123 | 124 | const createPersonalMessageHashFromData = (multisigAddr, transactionId, destinationAddr, value, data)=>{ 125 | return createPersonalMessageHash(createMsgHash(multisigAddr, transactionId, destinationAddr, value, data)); 126 | } 127 | /** 128 | * ECDSA sign 129 | * @param msgHash 130 | * @param privateKey 131 | * @return {Object} 132 | */ 133 | const signMessageHashUsingPrivateKey = (msgHash, privateKey)=>{ 134 | let sig = util.ecsign(new Buffer(util.stripHexPrefix(msgHash), 'hex'), new Buffer(privateKey, 'hex')); 135 | 136 | const sigV = sig.v; 137 | const sigR = '0x' + sig.r.toString('hex'); 138 | const sigS = '0x' + sig.s.toString('hex'); 139 | return {sigV: sigV, sigR: sigR, sigS: sigS} 140 | 141 | } 142 | 143 | const signMessageHashUsingPrivateKeyAndData = (multisigAddr, transactionId, destinationAddr, value, data, privateKey)=>{ 144 | return signMessageHashUsingPrivateKey(createPersonalMessageHashFromData(multisigAddr, transactionId, destinationAddr, value, data), privateKey); 145 | } 146 | 147 | const signMessageHashUsingWeb3Provider = async (address, msgHash, web3providerString="http://localhost:8545")=>{ 148 | const provider = new Web3.providers.HttpProvider(web3providerString); 149 | const web3 = new Web3(provider); 150 | let signature = await web3.eth.sign(msgHash.toString("hex"), address); 151 | 152 | var sig = util.fromRpcSig(signature.toString("hex")); 153 | const sigV = sig.v; 154 | const sigR = '0x' + sig.r.toString('hex'); 155 | const sigS = '0x' + sig.s.toString('hex'); 156 | console.log({sigV, sigR, sigS}); 157 | } 158 | 159 | const signMessageHashUsingWeb3ProviderAndData = (multisigAddr, transactionId, destinationAddr, value, data, address, web3providerString="http://localhost:8545")=>{ 160 | signMessageHashUsingWeb3Provider(address,createMsgHash(multisigAddr, transactionId, destinationAddr, value, data) , web3providerString); 161 | } 162 | 163 | const getSignHash = (msgHash, privateKey)=>{ 164 | 165 | var msg = Buffer.from(msgHash.replace('0x',''), 'hex'); 166 | //var msgHash = util.hashPersonalMessage(msg); 167 | var sgn = util.ecsign(msg, new Buffer(privateKey, "hex")); 168 | return util.toRpcSig(sgn.v, sgn.r, sgn.s); 169 | } 170 | 171 | const getUniqueId = () => { 172 | var ripemd160stream = new RIPEMD160(); 173 | ripemd160stream.end(Math.floor((Math.random() * 100) + 1)+""); 174 | var id = "0x" + ripemd160stream.read().toString('hex'); 175 | console.log(id); 176 | return id; 177 | } 178 | 179 | const getSplitPaymentHexData = (party1Address, party2Address, party1Percentage, party2Percentage, _transactionId)=>{ 180 | 181 | let data = lightwallet.txutils._encodeFunctionTxData('splitPayment', ['address[]', 'uint256[]', 'uint256'], [[party1Address, party2Address], [party1Percentage, party2Percentage], _transactionId]); 182 | return data; 183 | 184 | } 185 | 186 | if(command === "generateHash"){ 187 | if(argv.m === undefined || argv.t === undefined || argv.r === undefined || argv.v === undefined || argv.d === undefined){ 188 | console.log("Parameter Missing"); 189 | return; 190 | } 191 | else{ 192 | var hash = createMsgHash(argv.m, argv.t, argv.r, argv.v, argv.data); 193 | console.log(hash); 194 | } 195 | }else if(command === "generatePersonalMessageHash"){ 196 | if(argv.H === undefined && (argv.m === undefined || argv.t === undefined || argv.r === undefined || argv.v === undefined || argv.d === undefined)){ 197 | console.log("Parameter(s) Missing"); 198 | return; 199 | } 200 | else if(argv.H) { 201 | var hash = createPersonalMessageHash(argv.H); 202 | console.log(hash); 203 | } 204 | else{ 205 | var hash = createPersonalMessageHashFromData(argv.m, argv.t, argv.r, argv.v, argv.data); 206 | console.log(hash); 207 | } 208 | }else if(command === "signMessageUsingPrivateKey"){ 209 | if(argv.p === undefined || ((argv.e === undefined) && (argv.m === undefined || argv.t === undefined || argv.r === undefined || argv.v === undefined || argv.d === undefined))){ 210 | console.log("Parameter(s) Missing"); 211 | return; 212 | }else if(argv.e){ 213 | console.log(signMessageHashUsingPrivateKey(argv.e, argv.p)) 214 | }else{ 215 | console.log(signMessageHashUsingPrivateKeyAndData(argv.m, argv.t, argv.r, argv.v, argv.data, argv.p)); 216 | } 217 | }else if(command === "signMessageUsingLocalWeb3"){ 218 | if(argv.s === undefined || (argv.H === undefined && (argv.m === undefined || argv.t === undefined || argv.r === undefined || argv.v === undefined || argv.d === undefined))){ 219 | console.log("Parameter(s) Missing"); 220 | return; 221 | }else if(argv.H === undefined){ 222 | signMessageHashUsingWeb3ProviderAndData(argv.m, argv.t, argv.r, argv.v, argv.data, argv.s); 223 | } 224 | else{ 225 | signMessageHashUsingWeb3Provider(argv.s, argv.H); 226 | } 227 | }else if(command === "preimageHash"){ 228 | if(argv.i === undefined){ 229 | console.log("Parameter(s) Missing"); 230 | return; 231 | } 232 | else{ 233 | console.log(generatePreimageHash(argv.i)); 234 | } 235 | }else if(command === "splitPayments"){ 236 | if(argv.b === undefined || argv.S === undefined || argv.p === undefined || argv.P === undefined || argv.t === undefined){ 237 | console.log("Parameter(s) Missing"); 238 | } 239 | else{ 240 | console.log(getSplitPaymentHexData(argv.b, argv.S, argv.p, argv.P, argv.t)); 241 | } 242 | }else if(command === "generateMultiHash"){ 243 | if(argv.m === undefined || argv.r === undefined || argv.v === undefined || argv.k === undefined){ 244 | console.log("Parameter Missing"); 245 | return; 246 | } 247 | 248 | else{ 249 | var hash = createMultiMsgHash(argv.m, argv.k, argv.r, argv.v); 250 | console.log(hash); 251 | } 252 | }else if(command === "generateRedeemScript"){ 253 | if(argv.u === undefined || argv.T === undefined || argv.l === undefined || argv.b === undefined || argv.S === undefined || argv.M === undefined){ 254 | console.log("Parammeter(s) Missing"); 255 | } 256 | else{ 257 | console.log(generateRedeemScript(argv.u, argv.T, argv.l, argv.b, argv.S, argv.M)); 258 | } 259 | } 260 | else if(command === "getScriptHashFromData"){ 261 | if(argv.u === undefined || argv.T === undefined || argv.l === undefined || argv.b === undefined || argv.S === undefined || argv.M === undefined){ 262 | console.log("Parammeter(s) Missing"); 263 | } 264 | else{ 265 | console.log(getScriptHashFromData(argv.u, argv.T, argv.l, argv.b, argv.S, argv.M)); 266 | } 267 | } 268 | else if(command === "getScriptHash"){ 269 | if(argv.R === undefined){ 270 | console.log("Parameter(s) Missing"); 271 | }else{ 272 | console.log(getScriptHash(argv.R)); 273 | } 274 | } 275 | 276 | else if(command === "getUniqueId"){ 277 | var uniqueId = getUniqueId(); 278 | console.log(uniqueId); 279 | } 280 | 281 | else{ 282 | console.log('Command not recognized'); 283 | } 284 | 285 | 286 | 287 | 288 | //var msgHash = createMsgHash("0x546ba5c6abc490e66cb25a081868822c1ceb3abd", 24, "0x61adaf40a389761bacf76dfccf682e9200989894", 1000000000000000000, "0x"); 289 | //console.log(msgHash); 290 | 291 | //var personalMsgHash = createPersonalMessageHash(msgHash); 292 | //console.log(personalMsgHash); 293 | 294 | //var signs = signMessageHashUsingPrivateKey(personalMsgHash, "8954f0143159b26885799d99fe462498536f22ecd967aba64b677497bcd8eccb"); 295 | 296 | //console.log(signs); 297 | 298 | //signs = signMessageHashUsingWeb3Provider("0x4cb8543eaa17f648b79509e6c95caee0cee1cc49", msgHash, "http://localhost:8545" ); 299 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Exit script as soon as a command fails. 4 | set -o errexit 5 | 6 | # Executes cleanup function at script exit. 7 | trap cleanup EXIT 8 | 9 | cleanup() { 10 | # Kill the ganache instance that we started (if we started one and if it's still running). 11 | if [ -n "$ganache_pid" ] && ps -p $ganache_pid > /dev/null; then 12 | kill -9 $ganache_pid 13 | fi 14 | } 15 | 16 | if [ "$SOLIDITY_COVERAGE" = true ]; then 17 | ganache_port=8555 18 | else 19 | ganache_port=8555 20 | fi 21 | 22 | ganache_running() { 23 | nc -z localhost "$ganache_port" 24 | } 25 | 26 | start_ganache() { 27 | menomic_string="dog permit example repeat gloom defy teach pumpkin library remain scorpion skull" 28 | 29 | if [ "$SOLIDITY_COVERAGE" = true ]; then 30 | echo "Running Ganache CLI Coverage" 31 | node_modules/.bin/ganache-cli-coverage --emitFreeLogs true --allowUnlimitedContractSize true --gasLimit 0xfffffffffff --port "$ganache_port" -m "$menomic_string" -e 1000 -a 100 > /dev/null & 32 | else 33 | echo "Running Ganache" 34 | node_modules/.bin/ganache-cli --gasLimit 0xfffffffffff -m "$menomic_string" -e 1000 -a 100 -p $ganache_port > /dev/null & 35 | fi 36 | 37 | ganache_pid=$! 38 | } 39 | 40 | if ganache_running; then 41 | echo "Using existing ganache instance" 42 | 43 | else 44 | echo "Starting our own ganache instance" 45 | start_ganache 46 | 47 | while : 48 | do 49 | if ganache_running 50 | then 51 | break 52 | fi 53 | done 54 | echo "Ganache up and Running" 55 | fi 56 | 57 | if [ "$SOLC_NIGHTLY" = true ]; then 58 | echo "Downloading solc nightly" 59 | wget -q https://raw.githubusercontent.com/ethereum/solc-bin/gh-pages/bin/soljson-nightly.js -O /tmp/soljson.js && find . -name soljson.js -exec cp /tmp/soljson.js {} \; 60 | fi 61 | 62 | if [ "$SOLIDITY_COVERAGE" = true ]; then 63 | node_modules/.bin/solidity-coverage 64 | 65 | if [ "$CONTINUOUS_INTEGRATION" = true ]; then 66 | cat coverage/lcov.info | node_modules/.bin/coveralls 67 | fi 68 | else 69 | node_modules/.bin/truffle test "$@" 70 | fi -------------------------------------------------------------------------------- /scripts/usage.txt: -------------------------------------------------------------------------------- 1 | You can refer help command to check what all is allowed and meaning of each parameter 2 | 3 | node scripts/signining.js -h 4 | 5 | 1. Generate message hash using data 6 | 7 | node scripts/signing.js generateHash -m "0x546ba5c6abc490e66cb25a081868822c1ceb3abd" -r "0x61adaf40a389761bacf76dfccf682e9200989894" -t 24 -v 1000000000000000000 -d "0x" 8 | 9 | 10 | 2. Generate Personal Message Hash using message hash generated in previous step 11 | 12 | node scripts/signing.js generatePersonalMessageHash -H 0x9f58461e3ba1c1b24c92db44e8ebfb8c643b4e14e802bf735e15e52dbc355232 13 | 14 | 3. Generate Personal Message Hash using data 15 | 16 | node scripts/signing.js generatePersonalMessageHash -m "0x546ba5c6abc490e66cb25a081868822c1ceb3abd" -r "0x61adaf40a389761bacf76dfccf682e9200989894" -t 24 -v 1000000000000000000 -d "0x" 17 | 18 | 4. Sign message using private key and personal hash generated in previous step 19 | 20 | node scripts/signing.js signMessageUsingPrivateKey -e "5c0d2bebe012f64bf6c19599c2dc607c1e6f99318dd7cc903ded0519778f2275" -s "0x4cb8543eaa17f648b79509e6c95caee0cee1cc49" 21 | 22 | 5. Sign message using private key and data 23 | 24 | node scripts/signing.js signMessageUsingPrivateKey -m "0x546ba5c6abc490e66cb25a081868822c1ceb3abd" -r "0x61adaf40a389761bacf76dfccf682e9200989894" -t 24 -v 1000000000000000000 -d "0x" -p "8954f0143159b26885799d99fe462498536f22ecd967aba64b677497bcd8eccb" 25 | 26 | 6. Sign message using local web3 provider and message hash generated in step 1 27 | 28 | node scripts/signing.js signMessageUsingLocalWeb3 -H "0x9f58461e3ba1c1b24c92db44e8ebfb8c643b4e14e802bf735e15e52dbc355232" -s "0x4cb8543eaa17f648b79509e6c95caee0cee1cc49" 29 | 30 | 7. Sign message using local web3 provider and data 31 | 32 | node scripts/signing.js signMessageUsingLocalWeb3 -m "0x546ba5c6abc490e66cb25a081868822c1ceb3abd" -r "0x61adaf40a389761bacf76dfccf682e9200989894" -t 24 -v 1000000000000000000 -d "0x" -s "0x4cb8543eaa17f648b79509e6c95caee0cee1cc49" 33 | 34 | 8. Generate hash for preimage 35 | 36 | node scripts/signing.js preimageHash -i "sameep" 37 | 38 | 9. Generate splitpayments data 39 | 40 | node scripts/signing.js splitPayments -b 0x8B8d124EAE903DF72A01d7dDE9eA00E6c5938377 -S 0xa6Ac51BB2593e833C629A3352c4c432267714385 -p 30 -P 70 -t 11 41 | 42 | ####NOTE- Please do not send any funds to the addresses and private keys mentioned in this file. You may loose your funds. These are just for testing -------------------------------------------------------------------------------- /test/helper.js: -------------------------------------------------------------------------------- 1 | var RIPEMD160 = require('ripemd160'); 2 | var leftPad = require('left-pad'); 3 | var util = require("ethereumjs-util"); 4 | var lightwallet = require('eth-lightwallet') 5 | 6 | let keyFromPw 7 | let acct 8 | let lw 9 | 10 | const getUniqueId = () => { 11 | var ripemd160stream = new RIPEMD160(); 12 | var randomString = Math.floor((Math.random() * 100) + 1) + "" + Date.now(); 13 | ripemd160stream.end(randomString); 14 | var id = ripemd160stream.read().toString('hex'); 15 | return "0x" + id; 16 | } 17 | 18 | const generateRedeemScript = (uniqueId, threshold, timeoutHours, buyer, seller, moderator,multisigAddr, tokenAddress)=>{ 19 | let redeemScript = uniqueId + leftPad(threshold.toString(16), '2', '0') + leftPad(timeoutHours.toString(16), '8', '0') + buyer.slice(2) + seller.slice(2) + moderator.slice(2) + multisigAddr.slice(2) ; 20 | 21 | if(tokenAddress!=undefined){ 22 | redeemScript = redeemScript + tokenAddress.slice(2); 23 | } 24 | 25 | return redeemScript.toString('hex'); 26 | } 27 | 28 | const getScriptHash = (redeemScript) => { 29 | let hash = util.keccak256(redeemScript); 30 | return "0x" + hash.toString('hex'); 31 | } 32 | 33 | 34 | const createSigs = (signers, multisigAddr, destinationAddr, value, scriptHash) =>{ 35 | 36 | var dest= ""; 37 | var vals="" ; 38 | for(var i = 0;i { 63 | let sigV = [] 64 | let sigR = [] 65 | let sigS = [] 66 | 67 | for (var i=0; i { 78 | 79 | await web3.currentProvider.send({ 80 | jsonrpc: '2.0', 81 | method: 'evm_increaseTime', 82 | params: [seconds], 83 | id: new Date().getTime() 84 | }, (err, resp) => { 85 | 86 | }); 87 | 88 | await web3.currentProvider.send({ 89 | jsonrpc: '2.0', 90 | method: 'evm_mine', 91 | params: [], 92 | id: new Date().getTime() 93 | }, function(err, result){ 94 | }); 95 | } 96 | 97 | const setupWallet = ()=>{ 98 | 99 | let seed = "dog permit example repeat gloom defy teach pumpkin library remain scorpion skull"; 100 | return new Promise((resolve, reject)=>{ 101 | lightwallet.keystore.createVault( 102 | { 103 | hdPathString: "m/44'/60'/0'/0", 104 | seedPhrase: seed, 105 | password: "test", 106 | salt: "testsalt" 107 | }, 108 | function (err, keystore) { 109 | if(err){ 110 | reject(err); 111 | } 112 | lw = keystore; 113 | lw.keyFromPassword("test", async(e,k)=> { 114 | keyFromPw = k 115 | 116 | lw.generateNewAddress(keyFromPw, 100) 117 | let acctWithout0x = lw.getAddresses() 118 | acct = acctWithout0x.map((a) => {return a}) 119 | acct.sort(); 120 | resolve(acct); 121 | 122 | }) 123 | }); 124 | }); 125 | 126 | }; 127 | 128 | const keccak256 = (data)=>{ 129 | let hash = util.keccak256(data); 130 | return "0x" + hash.toString('hex'); 131 | }; 132 | 133 | 134 | module.exports = { 135 | getUniqueId, 136 | generateRedeemScript, 137 | getScriptHash, 138 | createSigs, 139 | increaseTime, 140 | setupWallet, 141 | keccak256, 142 | signMessageHash 143 | }; -------------------------------------------------------------------------------- /test/powerUps/3_PowerUps_tests.js: -------------------------------------------------------------------------------- 1 | var PowerUps = artifacts.require("PowerUps"); 2 | var OBToken = artifacts.require("OBToken"); 3 | var Web3 = require("web3"); 4 | var web3 = new Web3(); 5 | var BigNumber = require("bignumber.js"); 6 | 7 | contract("Keyword Based Powerups Contract", function(account) { 8 | var ids = new Array(); 9 | var powerUps = new Object(); 10 | var keywords = new Array(); 11 | var keywordVsPowerUpIds = new Object(); 12 | 13 | before(async () => { 14 | this.OBT = await OBToken.new("Open Bazaar", "OBT", 18, 1000000000, { 15 | from: account[0] 16 | }); 17 | 18 | this.keywordPowerup = await PowerUps.new(this.OBT.address); 19 | 20 | await this.OBT.transfer(account[1], web3.utils.toWei("1000000", "ether"), { 21 | from: account[0] 22 | }); 23 | await this.OBT.transfer(account[2], web3.utils.toWei("1000000", "ether"), { 24 | from: account[0] 25 | }); 26 | await this.OBT.transfer(account[3], web3.utils.toWei("1000000", "ether"), { 27 | from: account[0] 28 | }); 29 | await this.OBT.transfer(account[4], web3.utils.toWei("1000000", "ether"), { 30 | from: account[0] 31 | }); 32 | await this.OBT.transfer(account[5], web3.utils.toWei("1000000", "ether"), { 33 | from: account[0] 34 | }); 35 | await this.OBT.transfer(account[6], web3.utils.toWei("1000000", "ether"), { 36 | from: account[0] 37 | }); 38 | await this.OBT.transfer(account[7], web3.utils.toWei("1000000", "ether"), { 39 | from: account[0] 40 | }); 41 | 42 | keywords.push(web3.utils.fromAscii("open bazaar")); 43 | keywordVsPowerUpIds[keywords[0]] = new Array(); 44 | 45 | keywords.push(web3.utils.fromAscii("Christmas")); 46 | keywordVsPowerUpIds[keywords[1]] = new Array(); 47 | 48 | keywords.push(web3.utils.fromAscii("New Year")); 49 | keywordVsPowerUpIds[keywords[2]] = new Array(); 50 | 51 | keywords.push(web3.utils.fromAscii("Ethereum")); 52 | keywordVsPowerUpIds[keywords[3]] = new Array(); 53 | }); 54 | 55 | var addPowerup = async(initiator, contentAddress, amount, keyword) => { 56 | await this.OBT.approve(this.keywordPowerup.address, amount, { 57 | from: initiator 58 | }); 59 | var initiatorTokenBalanceBefore = await this.OBT.balanceOf(initiator); 60 | initiatorTokenBalanceBefore = new BigNumber(initiatorTokenBalanceBefore); 61 | 62 | var txResult = await this.keywordPowerup.addPowerUp( 63 | contentAddress, 64 | amount, 65 | keyword, 66 | { from: initiator } 67 | ); 68 | 69 | var receivedEvent = txResult.logs[0].event; 70 | var receivedinitiator = txResult.logs[0].args.initiator; 71 | var receivedId = txResult.logs[0].args.id; 72 | var receivedTokensBurnt = txResult.logs[0].args.tokensBurned; 73 | receivedTokensBurnt = new BigNumber(receivedTokensBurnt); 74 | assert.equal( 75 | receivedEvent, 76 | "NewPowerUpAdded", 77 | "NewPowerUpAdded event should be fired" 78 | ); 79 | assert.equal( 80 | receivedinitiator, 81 | initiator, 82 | "Received initiator and passed initiator must match" 83 | ); 84 | assert.equal( 85 | receivedTokensBurnt.toNumber(), 86 | amount, 87 | "Received and passed token amount to burn must match" 88 | ); 89 | 90 | var initiatorTokenBalanceAfter = await this.OBT.balanceOf(initiator); 91 | initiatorTokenBalanceAfter = new BigNumber(initiatorTokenBalanceAfter); 92 | 93 | assert.equal( 94 | initiatorTokenBalanceBefore.toNumber(), 95 | initiatorTokenBalanceAfter.plus(Number(amount)).toNumber(), 96 | "initiator's token balance must reduce by the amount sent" 97 | ); 98 | 99 | ids.push(receivedId); 100 | 101 | powerUps[receivedId] = new Object(); 102 | powerUps[receivedId].contentAddress = contentAddress; 103 | powerUps[receivedId].initiator = initiator; 104 | powerUps[receivedId].tokensBurnt = new BigNumber(amount); 105 | 106 | keywordVsPowerUpIds[keyword].push(receivedId); 107 | }; 108 | 109 | var addMultiplePowerups = async(initiator, contentAddress, amounts, keywords) => { 110 | 111 | var amount = new BigNumber(0); 112 | 113 | for(var i = 0;i { 172 | 173 | await this.OBT.approve(this.keywordPowerup.address, amount, { 174 | from: initiator 175 | }); 176 | 177 | var initiatorTokenBalanceBefore = await this.OBT.balanceOf(initiator); 178 | initiatorTokenBalanceBefore = new BigNumber(initiatorTokenBalanceBefore); 179 | 180 | var txResult = await this.keywordPowerup.topUpPowerUp(id, amount, { 181 | from: initiator 182 | }); 183 | 184 | var receivedEvent = txResult.logs[0].event; 185 | var receivedinitiator = txResult.logs[0].args.initiator; 186 | var receivedId = txResult.logs[0].args.id; 187 | var receivedTokensBurnt = txResult.logs[0].args.tokensBurned; 188 | receivedTokensBurnt = new BigNumber(receivedTokensBurnt); 189 | assert.equal(receivedEvent, "Topup", "Topup event should be fired"); 190 | assert.equal( 191 | receivedinitiator, 192 | initiator, 193 | "Received initiator and passed initiator must match" 194 | ); 195 | assert.equal( 196 | receivedId.toNumber(), 197 | id.toNumber(), 198 | "Received and passed id must match" 199 | ); 200 | assert.equal( 201 | receivedTokensBurnt.toNumber(), 202 | amount, 203 | "Received and passed token amount to burn must match" 204 | ); 205 | 206 | var initiatorTokenBalanceAfter = await this.OBT.balanceOf(initiator); 207 | initiatorTokenBalanceAfter = new BigNumber(initiatorTokenBalanceAfter); 208 | 209 | assert.equal( 210 | initiatorTokenBalanceBefore.toNumber(), 211 | initiatorTokenBalanceAfter.plus(Number(amount)).toNumber(), 212 | "initiator's token balance must reduce by the amount sent" 213 | ); 214 | powerUps[id].tokensBurnt = powerUps[id].tokensBurnt.plus(new BigNumber(amount)); 215 | }; 216 | 217 | it("Add new Power up", async () => { 218 | var initiator = account[1]; 219 | var contentAddress = "QmTDMoVqvyBkNMRhzvukTDznntByUNDwyNdSfV8dZ3VKRC"; 220 | var amount = web3.utils.toWei("1000", "ether"); 221 | var keyword = keywords[0]; 222 | 223 | await addPowerup(initiator, contentAddress, amount, keyword); 224 | }); 225 | 226 | it("Add new power up with tokens to burn as 0", async () => { 227 | var initiator = account[1]; 228 | var contentAddress = 229 | "GFBasuH1829mTDMoVqvyBkNMRhzvukTDznnUY76yhnJKJgFhKlIhGFf678dZ3VKRC"; 230 | var amount = 0; 231 | var keyword = keywords[0]; 232 | 233 | try { 234 | await addPowerup(initiator, contentAddress, amount, keyword); 235 | assert.equal(true, false, "Add listing must fail for amount 0"); 236 | } catch (error) { 237 | assert.notInclude(error.toString(), "AssertionError", error.message); 238 | } 239 | }); 240 | 241 | it("Add new power up with content address as empty string", async () => { 242 | var initiator = account[2]; 243 | var contentAddress = ""; 244 | var amount = web3.utils.toWei("1000", "ether"); 245 | var keyword = keywords[0]; 246 | 247 | try { 248 | await addPowerup(initiator, contentAddress, amount, keyword); 249 | assert.equal( 250 | true, 251 | false, 252 | "Add listing must fail for empyt content address" 253 | ); 254 | } catch (error) { 255 | assert.notInclude(error.toString(), "AssertionError", error.message); 256 | } 257 | }); 258 | 259 | it("Add new power up without apporving contract to burn tokens on behalf of initiator", async () => { 260 | var initiator = account[3]; 261 | var contentAddress = "AjhHKKhaKJHAKJshASHAshaSKHAjASHALHAHAKKJJKKJhjlkJK"; 262 | var amount = web3.utils.toWei("1000", "ether"); 263 | var keyword = keywords[0]; 264 | 265 | try { 266 | await this.keywordPowerup.addPowerUp(contentAddress, amount, keyword, { 267 | from: initiator 268 | }); 269 | assert.equal( 270 | true, 271 | false, 272 | "Add listing must fail without approving powered listing contract to burn tokens on behalf of initiator" 273 | ); 274 | } catch (error) { 275 | assert.notInclude(error.toString(), "AssertionError", error.message); 276 | } 277 | }); 278 | 279 | it("Add power up with zero address as initiator", async () => { 280 | var initiator = "0x0000000000000000000000000000000000000000"; 281 | var contentAddress = "AjhHKKhaKJHAKJshASHAshaSKHAjASHALHAHAKKJJKKJhjlkJK"; 282 | var amount = web3.utils.toWei("1000", "ether"); 283 | var keyword = keywords[0]; 284 | 285 | try { 286 | await addPowerup(initiator, contentAddress, amount, keyword); 287 | assert.equal( 288 | true, 289 | false, 290 | "Add listing must fail with initiator as zero address" 291 | ); 292 | } catch (error) { 293 | assert.notInclude(error.toString(), "AssertionError", error.message); 294 | } 295 | }); 296 | 297 | it("Add multiple power-ups with different keyword for same content address", async () => { 298 | var contentAddress = 299 | "TmTDMoVqvyBkNMRhzvukTDznntByUNgkhasdgashdjhd0892138291038DwyNdSfV8dZ3VKRC"; 300 | 301 | var initiator = account[3]; 302 | var amounts = new Array(); 303 | 304 | amounts.push(web3.utils.toWei("1000", "ether")); 305 | amounts.push(web3.utils.toWei("2000", "ether")); 306 | amounts.push(web3.utils.toWei("3000", "ether")); 307 | amounts.push(web3.utils.toWei("4000", "ether")); 308 | 309 | await addMultiplePowerups(initiator, contentAddress, amounts, keywords); 310 | }); 311 | 312 | it("Topup power up", async () => { 313 | var initiator = powerUps[ids[0]].initiator; 314 | var id = ids[0]; 315 | var amount = web3.utils.toWei("2000", "ether"); 316 | 317 | await topup(initiator, id, amount); 318 | }); 319 | 320 | it("Topup non-existing powerup", async () => { 321 | var initiator = powerUps[ids[0]].initiator; 322 | var id = 29; 323 | var amount = web3.utils.toWei("2000", "ether"); 324 | 325 | try { 326 | await topup(initiator, id, amount); 327 | assert.equal(true, false, "Topup must fail for non-existing powerup"); 328 | } catch (error) { 329 | assert.notInclude(error.toString(), "AssertionError", error.message); 330 | } 331 | }); 332 | 333 | it("Topup listing without approving contract to burn tokens on behalf of initiator", async () => { 334 | var initiator = powerUps[ids[0]].initiator; 335 | var id = ids[0]; 336 | var amount = web3.utils.toWei("7000", "ether"); 337 | 338 | try { 339 | await this.keywordPowerup.topUpPowerUp(id, amount, { from: initiator }); 340 | assert.equal( 341 | true, 342 | false, 343 | "Topup must fail without approving contract to burn on behalf of initiator" 344 | ); 345 | } catch (error) { 346 | assert.notInclude(error.toString(), "AssertionError", error.message); 347 | } 348 | }); 349 | 350 | it("Top up listing with amount of tokens to burn as 0", async () => { 351 | var initiator = powerUps[ids[0]].initiator; 352 | var id = ids[0]; 353 | var amount = 0; 354 | 355 | try { 356 | await topup(initiator, id, amount); 357 | assert.equal(true, false, "Topup must fail with topup amount as 0"); 358 | } catch (error) { 359 | assert.notInclude(error.toString(), "AssertionError", error.message); 360 | } 361 | }); 362 | 363 | it("Get power up information for existing power up", async () => { 364 | var id = ids[0] 365 | var powerUpInfo = await this.keywordPowerup.getPowerUpInfo(id); 366 | var receivedContentAddress = powerUpInfo[0]; 367 | var receivedTokensBurnt = powerUpInfo[1]; 368 | receivedTokensBurnt = new BigNumber(receivedTokensBurnt); 369 | assert.equal( 370 | receivedContentAddress, 371 | powerUps[ids[0]].contentAddress, 372 | "Recieved and passed content address must match" 373 | ); 374 | assert.equal( 375 | receivedTokensBurnt.toNumber(), 376 | powerUps[ids[0]].tokensBurnt, 377 | "Recived tokens burnt must match total tokens burnt for this listing" 378 | ); 379 | }); 380 | 381 | it("Get power up information for non-existing power up", async () => { 382 | var powerUpInfo = await this.keywordPowerup.getPowerUpInfo("29"); 383 | var receivedContentAddress = powerUpInfo[0]; 384 | var receivedTokensBurnt = powerUpInfo[1]; 385 | 386 | assert.equal( 387 | receivedContentAddress, 388 | "", 389 | "Recieved content address must be empty" 390 | ); 391 | assert.equal(receivedTokensBurnt, 0, "Recived tokens burnt must be zero"); 392 | }); 393 | }); 394 | -------------------------------------------------------------------------------- /test/registry/2_contract_manager_test.js: -------------------------------------------------------------------------------- 1 | var ContractManager = artifacts.require("ContractManager"); 2 | var Escrow = artifacts.require("Escrow"); 3 | 4 | contract("Contract Manager contract", function(accounts) { 5 | var ownerAccount = accounts[0]; 6 | var contracts = new Array(); 7 | var contractVersions = new Object(); 8 | var versions = new Array(); 9 | 10 | before(async () => { 11 | this.contractManager = await ContractManager.new({ from: ownerAccount }); 12 | 13 | contracts[0] = "escrow"; 14 | versions[0] = new Array(); 15 | versions[0][0] = "v1.0"; 16 | versions[0][1] = "v1.1"; 17 | }); 18 | 19 | var addVersion = async(contractName, version, status, _implementation) => { 20 | var txResult = await this.contractManager.addVersion( 21 | contractName, 22 | version, 23 | status, 24 | _implementation 25 | ); 26 | var eventName = txResult.logs[0].event; 27 | var receivedContractName = txResult.logs[0].args.contractName; 28 | var receivedVersion = txResult.logs[0].args.versionName; 29 | var receivedImplementation = txResult.logs[0].args.implementation; 30 | 31 | assert.equal(eventName, "VersionAdded", "VersionAdded event must be fired"); 32 | assert.equal( 33 | receivedContractName, 34 | contractName, 35 | "Received contract name must match the passed one" 36 | ); 37 | assert.equal( 38 | receivedVersion, 39 | version, 40 | "Received version must match the passed one" 41 | ); 42 | assert.equal( 43 | receivedImplementation, 44 | _implementation, 45 | "Received implementation must match the passed one" 46 | ); 47 | 48 | var versionData = await this.contractManager.getVersionDetails( 49 | contractName, 50 | version 51 | ); 52 | receivedVersion = versionData[0]; 53 | var receivedStatus = versionData[1].toNumber(); 54 | var receivedbugLevel = versionData[2].toNumber(); 55 | receivedImplementation = versionData[3]; 56 | 57 | assert.equal( 58 | receivedVersion, 59 | version, 60 | "Received version must match the passed one" 61 | ); 62 | assert.equal( 63 | receivedStatus, 64 | status, 65 | "Received staus must be the one which was passed" 66 | ); 67 | assert.equal(receivedbugLevel, 0, "Received bug level must be NONE(0)"); 68 | assert.equal( 69 | receivedImplementation, 70 | _implementation, 71 | "Received implementation must match the passed one" 72 | ); 73 | 74 | if(contractVersions[contractName] === undefined){ 75 | contractVersions[contractName] = new Object(); 76 | } 77 | contractVersions[contractName][version] = new Object(); 78 | contractVersions[contractName][version].version = receivedVersion; 79 | contractVersions[contractName][version].status = receivedStatus; 80 | contractVersions[contractName][version].bugLevel = receivedbugLevel; 81 | contractVersions[contractName][version].implementation = receivedImplementation; 82 | }; 83 | 84 | var updateVersion = async(contractName, version, status, bugLevel) => { 85 | var txResult = await this.contractManager.updateVersion( 86 | contractName, 87 | version, 88 | status, 89 | bugLevel 90 | ); 91 | 92 | var eventName = txResult.logs[0].event; 93 | var receivedContractName = txResult.logs[0].args.contractName; 94 | var receivedVersion = txResult.logs[0].args.versionName; 95 | var receivedStatus = txResult.logs[0].args.status; 96 | var receivedBugLevel = txResult.logs[0].args.bugLevel; 97 | 98 | assert.equal(eventName, "VersionUpdated", "VersionUpdated must be fired"); 99 | assert.equal( 100 | receivedVersion, 101 | version, 102 | "Received version must match the passed one" 103 | ); 104 | assert.equal( 105 | receivedStatus, 106 | status, 107 | "Received status must be the one which was passed" 108 | ); 109 | assert.equal( 110 | receivedContractName, 111 | contractName, 112 | "Received contract name must match the passed contract name" 113 | ); 114 | assert.equal( 115 | receivedBugLevel, 116 | bugLevel, 117 | "Received bug level must be the one which was passed" 118 | ); 119 | 120 | var versionData = await this.contractManager.getVersionDetails( 121 | contractName, 122 | version 123 | ); 124 | 125 | receivedStatus = versionData[1].toNumber(); 126 | 127 | assert.equal( 128 | receivedStatus, 129 | status, 130 | "Received staus must be correctly set in the storage" 131 | ); 132 | contractVersions[contractName][version].status = status; 133 | }; 134 | 135 | var markRecommended = async(contractName, version) => { 136 | var txResult = await this.contractManager.markRecommendedVersion( 137 | contractName, 138 | version 139 | ); 140 | 141 | var eventName = txResult.logs[0].event; 142 | var receivedContractName = txResult.logs[0].args.contractName; 143 | var receivedVersion = txResult.logs[0].args.versionName; 144 | 145 | assert.equal( 146 | eventName, 147 | "VersionRecommended", 148 | "VersionRecommended must be fired" 149 | ); 150 | assert.equal( 151 | receivedVersion, 152 | version, 153 | "Received version must match the passed one" 154 | ); 155 | assert.equal( 156 | receivedContractName, 157 | contractName, 158 | "Received contract name must match the passed contract name" 159 | ); 160 | 161 | var versionData = await this.contractManager.getRecommendedVersion( 162 | contractName 163 | ); 164 | 165 | receivedVersion = versionData[0]; 166 | 167 | var receivedImplementation = versionData[3]; 168 | 169 | assert.equal( 170 | receivedVersion, 171 | version, 172 | "Received version must match the passed one" 173 | ); 174 | 175 | assert.equal( 176 | receivedImplementation, 177 | contractVersions[contractName][version].implementation, 178 | "Recommended version's implementation address must with the actual set one" 179 | ); 180 | }; 181 | 182 | var removeRecommendedVersion = async(contractName) => { 183 | var versionData = await this.contractManager.getRecommendedVersion( 184 | contractName 185 | ); 186 | 187 | var version = versionData[0]; 188 | 189 | var txResult = await this.contractManager.removeRecommendedVersion( 190 | contractName 191 | ); 192 | 193 | var eventName = txResult.logs[0].event; 194 | var receivedContractName = txResult.logs[0].args.contractName; 195 | 196 | assert.equal( 197 | eventName, 198 | "RecommendedVersionRemoved", 199 | "RecommendedVersionRemoved must be fired" 200 | ); 201 | assert.equal( 202 | receivedContractName, 203 | contractName, 204 | "Received contract name must match the passed contract name" 205 | ); 206 | 207 | var versionData = await this.contractManager.getRecommendedVersion( 208 | contractName 209 | ); 210 | 211 | var receivedVersion = versionData[0]; 212 | 213 | assert.equal( 214 | receivedVersion, 215 | "", 216 | "Recommended version should have been removed" 217 | ); 218 | 219 | versionData = await this.contractManager.getVersionDetails( 220 | contractName, 221 | version 222 | ); 223 | 224 | var receivedVersion = versionData[0]; 225 | var receivedStatus = versionData[1].toNumber(); 226 | var receivedbugLevel = versionData[2].toNumber(); 227 | var receivedImplementation = versionData[3]; 228 | 229 | assert.equal( 230 | receivedVersion, 231 | contractVersions[contractName][version].version, 232 | "Received version must match the passed one" 233 | ); 234 | assert.equal( 235 | receivedStatus, 236 | contractVersions[contractName][version].status, 237 | "Received staus must be the one which was passed" 238 | ); 239 | assert.equal( 240 | receivedbugLevel, 241 | contractVersions[contractName][version].bugLevel, 242 | "Received bug level must match" 243 | ); 244 | assert.equal( 245 | receivedImplementation, 246 | contractVersions[contractName][version].implementation, 247 | "Received implementation must match the passed one" 248 | ); 249 | }; 250 | 251 | it("Add version v1.0 for escrow contract", async () => { 252 | var escrowVersion1_0 = await Escrow.new(); 253 | var contractName = contracts[0]; 254 | var version = versions[0][0]; 255 | var status = 0; 256 | var _implementation = escrowVersion1_0.address; 257 | 258 | await addVersion(contractName, version, status, _implementation); 259 | }); 260 | 261 | it("Add version v1.1 for escrow contract", async () => { 262 | var escrowVersion1_1 = await Escrow.new(); 263 | var contractName = contracts[0]; 264 | var version = versions[0][1]; 265 | var status = 0; 266 | var _implementation = escrowVersion1_1.address; 267 | 268 | await addVersion(contractName, version, status, _implementation); 269 | }); 270 | 271 | it("Add version v1.1 again for escrow contract", async () => { 272 | var escrowVersion1_1 = await Escrow.new(); 273 | var contractName = contracts[0]; 274 | var version = versions[0][1]; 275 | var status = 0; 276 | var _implementation = escrowVersion1_1.address; 277 | 278 | try { 279 | await addVersion(contractName, version, status, _implementation); 280 | assert.equal( 281 | true, 282 | false, 283 | "Should not be able to add version with v1.1 as it is already registered" 284 | ); 285 | } catch (error) { 286 | assert.notInclude(error.toString(), "AssertionError", error.message); 287 | } 288 | }); 289 | 290 | it("Add version with empty contract name", async () => { 291 | var escrowVersion1_1 = await Escrow.new(); 292 | var contractName = ""; 293 | var version = versions[0][1]; 294 | var status = 0; 295 | var _implementation = escrowVersion1_1.address; 296 | 297 | try { 298 | await addVersion(contractName, version, status, _implementation); 299 | assert.equal( 300 | true, 301 | false, 302 | "Should not be able to add version with empty contract name" 303 | ); 304 | } catch (error) { 305 | assert.notInclude(error.toString(), "AssertionError", error.message); 306 | } 307 | }); 308 | 309 | it("Add version with empty version name", async () => { 310 | var escrowVersion1_1 = await Escrow.new(); 311 | var contractName = contracts[0]; 312 | var version = ""; 313 | var status = 0; 314 | var _implementation = escrowVersion1_1.address; 315 | 316 | try { 317 | await addVersion(contractName, version, status, _implementation); 318 | assert.equal( 319 | true, 320 | false, 321 | "Should not be able to version with empty version name" 322 | ); 323 | } catch (error) { 324 | assert.notInclude(error.toString(), "AssertionError", error.message); 325 | } 326 | }); 327 | 328 | it("Change status of version to PRODUCTION", async () => { 329 | var contractName = contracts[0]; 330 | var version = versions[0][1]; 331 | var status = 2; //PRODUCTION; 332 | var bugLevel = contractVersions[contractName][version].bugLevel; 333 | 334 | await updateVersion(contractName, version, status, bugLevel); 335 | }); 336 | 337 | it("Change bug level of version to HIGH", async () => { 338 | var contractName = contracts[0]; 339 | var version = versions[0][1]; 340 | var bugLevel = 3; //HIGH; 341 | var status = contractVersions[contractName][version].status; 342 | 343 | await updateVersion(contractName, version, status, bugLevel); 344 | }); 345 | 346 | it("Change bug level of version to NONE", async () => { 347 | var contractName = contracts[0]; 348 | var version = versions[0][1]; 349 | var bugLevel = 0; //NONE; 350 | var status = contractVersions[contractName][version].status; 351 | 352 | await updateVersion(contractName, version, status, bugLevel); 353 | }); 354 | 355 | it("Mark version as recommended", async () => { 356 | var contractName = contracts[0]; 357 | var version = versions[0][1]; 358 | 359 | await markRecommended(contractName, version); 360 | }); 361 | 362 | it("Change status of version to BETA", async () => { 363 | var contractName = contracts[0]; 364 | var version = versions[0][1]; 365 | var status = 0; //BETA; 366 | var bugLevel = contractVersions[contractName][version].bugLevel; 367 | 368 | await updateVersion(contractName, version, status, bugLevel); 369 | }); 370 | 371 | it("Change status of version to RC", async () => { 372 | var contractName = contracts[0]; 373 | var version = versions[0][1]; 374 | var status = 1; //RC; 375 | var bugLevel = contractVersions[contractName][version].bugLevel; 376 | 377 | await updateVersion(contractName, version, status, bugLevel); 378 | }); 379 | 380 | it("Change status of version to DEPRECATED", async () => { 381 | var contractName = contracts[0]; 382 | var version = versions[0][1]; 383 | var status = 3; //DEPRECATED; 384 | var bugLevel = contractVersions[contractName][version].bugLevel; 385 | 386 | await updateVersion(contractName, version, status, bugLevel); 387 | }); 388 | 389 | it("Change bug level of version to LOW", async () => { 390 | var contractName = contracts[0]; 391 | var version = versions[0][1]; 392 | var bugLevel = 1; //LOW; 393 | var status = contractVersions[contractName][version].status; 394 | 395 | await updateVersion(contractName, version, status, bugLevel); 396 | }); 397 | 398 | it("Change bug level of version to MEDIUM", async () => { 399 | var contractName = contracts[0]; 400 | var version = versions[0][1]; 401 | var bugLevel = 2; //MEDIUM; 402 | var status = contractVersions[contractName][version].status; 403 | 404 | await updateVersion(contractName, version, status, bugLevel); 405 | }); 406 | 407 | it("Change bug level of version to CRITICAL", async () => { 408 | var contractName = contracts[0]; 409 | var version = versions[0][1]; 410 | var bugLevel = 4; //CRITICAL; 411 | var status = contractVersions[contractName][version].status; 412 | 413 | await updateVersion(contractName, version, status, bugLevel); 414 | }); 415 | 416 | it("Change bug level of version to NONE", async () => { 417 | var contractName = contracts[0]; 418 | var version = versions[0][1]; 419 | var bugLevel = 0; //NONE; 420 | var status = contractVersions[contractName][version].status; 421 | 422 | await updateVersion(contractName, version, status, bugLevel); 423 | }); 424 | 425 | it("Remove recommended version", async () => { 426 | var contractName = contracts[0]; 427 | 428 | await removeRecommendedVersion(contractName); 429 | }); 430 | 431 | it("Add version with implementation address as non-contract address", async () => { 432 | var contractName = contracts[0]; 433 | var version = "v1.2"; 434 | var status = 0; 435 | var _implementation = accounts[7]; 436 | 437 | try { 438 | await addVersion(contractName, version, status, _implementation); 439 | assert.equal( 440 | true, 441 | false, 442 | "Should not be able to add version with implementaion address as non-contract address" 443 | ); 444 | } catch (error) { 445 | assert.notInclude(error.toString(), "AssertionError", error.message); 446 | } 447 | }); 448 | 449 | it("Check total contract count", async () => { 450 | var count = await this.contractManager.getTotalContractCount(); 451 | 452 | assert.equal( 453 | count.toNumber(), 454 | contracts.length, 455 | "Number of contracts does not match the contract registered" 456 | ); 457 | }); 458 | 459 | it("Check registered contracts", async () => { 460 | var count = await this.contractManager.getTotalContractCount(); 461 | 462 | for (var i = 0; i < count.toNumber(); i++) { 463 | var contractName = await this.contractManager.getContractAtIndex(i); 464 | assert.equal( 465 | contracts[i], 466 | contractName, 467 | "Contract name must match the registered contract" 468 | ); 469 | } 470 | }); 471 | 472 | it("Check total count of versions for a contract", async () => { 473 | var count = await this.contractManager.getTotalContractCount(); 474 | 475 | for (var i = 0; i < count.toNumber(); i++) { 476 | var contractName = await this.contractManager.getContractAtIndex(i); 477 | 478 | var versionCount = await this.contractManager.getVersionCountForContract( 479 | contractName 480 | ); 481 | assert.equal( 482 | versionCount.toNumber(), 483 | Object.keys(contractVersions[contractName]).length, 484 | "Number of contracts versions does not match the versions registered for contract: "+ contractName 485 | ); 486 | } 487 | }); 488 | 489 | it("Check registered versions for a contract", async () => { 490 | var contractCount = await this.contractManager.getTotalContractCount(); 491 | 492 | for (var i = 0; i < contractCount.toNumber(); i++) { 493 | var contractName = await this.contractManager.getContractAtIndex(i); 494 | 495 | var versionCount = await this.contractManager.getVersionCountForContract( 496 | contractName 497 | ); 498 | 499 | for (var j = 0; j < versionCount.toNumber(); j++) { 500 | var versionName = await this.contractManager.getVersionAtIndex( 501 | contractName, 502 | j 503 | ); 504 | 505 | var versionData = await this.contractManager.getVersionDetails( 506 | contractName, 507 | versionName 508 | ); 509 | 510 | var receivedVersion = versionData[0]; 511 | var receivedStatus = versionData[1].toNumber(); 512 | var receivedbugLevel = versionData[2].toNumber(); 513 | var receivedImplementation = versionData[3]; 514 | 515 | assert.equal( 516 | receivedVersion, 517 | contractVersions[contractName][versionName].version, 518 | "Received version must match the passed one" 519 | ); 520 | assert.equal( 521 | receivedStatus, 522 | contractVersions[contractName][versionName].status, 523 | "Received staus must be the one which was passed" 524 | ); 525 | assert.equal( 526 | receivedbugLevel, 527 | contractVersions[contractName][versionName].bugLevel, 528 | "Received bug level must match" 529 | ); 530 | assert.equal( 531 | receivedImplementation, 532 | contractVersions[contractName][versionName].implementation, 533 | "Received implementation must match the passed one" 534 | ); 535 | } 536 | } 537 | }); 538 | }); -------------------------------------------------------------------------------- /test/rewards/6_OB_Rewards_Test.js: -------------------------------------------------------------------------------- 1 | var OBRewards = artifacts.require("OBRewards"); 2 | var OBToken = artifacts.require("OBToken"); 3 | var Escrow = artifacts.require("Escrow"); 4 | var EscrowProxy = artifacts.require("EscrowProxy"); 5 | 6 | var helper = require("../helper.js"); 7 | var Web3 = require("web3"); 8 | var web3 = new Web3("http://localhost:8555"); 9 | var BigNumber = require('bignumber.js'); 10 | 11 | let acct; 12 | var buyers = new Array(); 13 | var nonPromotedSellers = new Array(); 14 | var promotedSellers = new Array(); 15 | var moderators = new Array(); 16 | 17 | contract("OB Rewards Contract", function() { 18 | 19 | var createCompletedTransactionsWithPromotedSellers = async(start, numberOfTransactions)=>{ 20 | var transactions = new Object(); 21 | 22 | for (var i = 0;i{ 51 | var transactions = new Object(); 52 | 53 | for (var i = 0;i { 82 | acct = await helper.setupWallet(); 83 | 84 | //Push promoted sellers 85 | for(var i = 0;i<10; i++){ 86 | promotedSellers.push(acct[1+i]); 87 | } 88 | 89 | //push non promoted seller accounts 90 | for(var i = 0;i<10;i++){ 91 | nonPromotedSellers.push(acct[11+i]); 92 | } 93 | 94 | //push moderator accounts 95 | for(var i = 0;i<10;i++){ 96 | moderators.push(acct[21+i]); 97 | } 98 | 99 | //Push buyer accounts 100 | for(var i = 0;i<1500;i++){ 101 | buyers.push(acct[31+i]); 102 | } 103 | 104 | this.escrow = await Escrow.new({from:acct[0]}); 105 | 106 | this.OBT = await OBToken.new("Open Bazaar", "OBT", 18, "100000000", {from:acct[0]}); 107 | 108 | this.rewards = await OBRewards.new("500000000000000000000", 432000, this.escrow.address, this.OBT.address, {from:acct[0]}); 109 | await this.rewards.addPromotedSellers(promotedSellers, {from:acct[0]}); 110 | 111 | await this.OBT.transfer(this.rewards.address, "570000000000000000000", {from:acct[0]}); 112 | this.escrowProxy = await EscrowProxy.new("0x0000000000000000000000000000000000000000"); 113 | }); 114 | 115 | it("Claim reward for transaction when rewards is not on", async()=>{ 116 | var transactions = await createCompletedTransactionsWithPromotedSellers(0, 1); 117 | var scriptHashes = new Array(); 118 | 119 | for(var key in transactions){ 120 | scriptHashes.push(key); 121 | } 122 | try{ 123 | await this.rewards.claimRewards(scriptHashes); 124 | assert.equal(true, false, "Should not be able to claim rewards when rewards is not on"); 125 | }catch(error){ 126 | assert.notInclude(error.toString(), 'AssertionError', error.message); 127 | } 128 | }); 129 | 130 | it("Turn rewards on from non-owner account", async()=>{ 131 | try{ 132 | await this.rewards.turnOnRewards({from:acct[1]}); 133 | assert.equal(true, false, "Should not be able to turn on rewards from non-owner account"); 134 | }catch(error){ 135 | assert.notInclude(error.toString(), 'AssertionError', error.message); 136 | } 137 | }); 138 | 139 | it("Turn rewards on from owner account", async()=>{ 140 | var txResult = await this.rewards.turnOnRewards({from:acct[0]}); 141 | var event = txResult.logs[0].event; 142 | assert.equal(event, "RewardsOn", "RewardsOn event must be fired"); 143 | var rewardsOn = await this.rewards.rewardsOn(); 144 | assert(rewardsOn, true, "Rewards must be on"); 145 | }); 146 | 147 | it("Claim reward when rewards are not running", async()=>{ 148 | var transactions = await createCompletedTransactionsWithPromotedSellers(0, 1); 149 | var scriptHashes = new Array(); 150 | for(var key in transactions){ 151 | scriptHashes.push(key); 152 | } 153 | try{ 154 | await this.rewards.claimRewards(scriptHashes); 155 | assert.equal(true, false, "Should not be able to claim rewards when rewards are not running"); 156 | }catch(error){ 157 | assert.notInclude(error.toString(), 'AssertionError', error.message); 158 | } 159 | }); 160 | 161 | it("Set end date from non-owner account", async()=>{ 162 | var endDate = new Date(); 163 | endDate.setDate(endDate.getDate()+2); 164 | 165 | try{ 166 | await this.rewards.setEndDate(endDate.getTime()/1000, {from:acct[100]}); 167 | assert.equal(true, false, "Should not be able to change end date from non-owner account"); 168 | }catch(error){ 169 | assert.notInclude(error.toString(), 'AssertionError', error.message); 170 | } 171 | }); 172 | 173 | it("Set end date to some point in the future", async()=>{ 174 | var endDate = new Date(); 175 | endDate.setDate(endDate.getDate()+2); 176 | 177 | var txResult = await this.rewards.setEndDate(Math.floor(endDate.getTime()/1000), {from:acct[0]}); 178 | var event = txResult.logs[0].event; 179 | var receivedEndDate = txResult.logs[0].args.endDate; 180 | assert.equal(event, "EndDateChanged", "EndDateChanged event must be fired"); 181 | assert.equal(receivedEndDate.toNumber(), Math.floor(endDate.getTime()/1000), "Passed and received end date must match"); 182 | 183 | var running = await this.rewards.isRewardsRunning(); 184 | assert(running, true, "Rewards must be running now"); 185 | }); 186 | 187 | it("Claim reward for 1 valid scriptHashes", async()=>{ 188 | var transactions = await createCompletedTransactionsWithPromotedSellers(0, 1); 189 | var scriptHashes = new Array(); 190 | for(var key in transactions){ 191 | scriptHashes.push(key); 192 | } 193 | var txResult = await this.rewards.claimRewards(scriptHashes); 194 | 195 | for(var i=0;i{ 207 | var transactions = await createCompletedTransactionsWithPromotedSellers(1, 10); 208 | var scriptHashes = new Array(); 209 | 210 | for(var key in transactions){ 211 | scriptHashes.push(key); 212 | } 213 | var txResult = await this.rewards.claimRewards(scriptHashes); 214 | for(var i=0;i{ 227 | var transactions = await createCompletedTransactionsWithPromotedSellers(11, 1); 228 | var scriptHashes = new Array(); 229 | var buyer; 230 | 231 | for(var key in transactions){ 232 | scriptHashes.push(key); 233 | buyer = transactions[key].buyer; 234 | } 235 | var previousBuyerBalance = await this.OBT.balanceOf(buyer); 236 | previousBuyerBalance=new BigNumber(previousBuyerBalance); 237 | var contractTokenBalance = await this.OBT.balanceOf(this.rewards.address); 238 | contractTokenBalance = new BigNumber(contractTokenBalance); 239 | var txResult = await this.rewards.claimRewards(scriptHashes); 240 | for(var i=0;i{ 256 | await this.OBT.transfer(this.rewards.address, "20000000000000000000", {from:acct[0]}); 257 | var transactions = await createCompletedTransactionsWithPromotedSellers(12, 1); 258 | var scriptHashes = new Array(); 259 | var buyer; 260 | 261 | for(var key in transactions){ 262 | scriptHashes.push(key); 263 | buyer = transactions[key].buyer; 264 | } 265 | var previousBuyerBalance = await this.OBT.balanceOf(buyer); 266 | previousBuyerBalance = new BigNumber(previousBuyerBalance); 267 | var contractTokenBalance = await this.OBT.balanceOf(this.rewards.address); 268 | contractTokenBalance = new BigNumber(contractTokenBalance); 269 | var remainingTokens = 50000000000000000000 - contractTokenBalance.toNumber(); 270 | var txResult = await this.rewards.claimRewards(scriptHashes); 271 | for(var i=0;i{ 304 | var transactions = await createCompletedTransactionsWithNonPromotedSellers(0, 1); 305 | var scriptHashes = new Array(); 306 | 307 | for(var key in transactions){ 308 | scriptHashes.push(key); 309 | } 310 | var txResult = await this.rewards.claimRewards(scriptHashes); 311 | for(var i=0;i{ 324 | var transactions = await createCompletedTransactionsWithNonPromotedSellers(1, 10); 325 | var scriptHashes = new Array(); 326 | 327 | for(var key in transactions){ 328 | scriptHashes.push(key); 329 | } 330 | var txResult = await this.rewards.claimRewards(scriptHashes); 331 | for(var i=0;i