├── .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 | [](https://travis-ci.org/OpenBazaar/smart-contracts)
3 |
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