diff --git a/contracts/DAppStore.sol b/contracts/DAppStore.sol index 098d925..b057d43 100644 --- a/contracts/DAppStore.sol +++ b/contracts/DAppStore.sol @@ -12,19 +12,19 @@ contract DAppStore is ApproveAndCallFallBack, BancorFormula { MiniMeTokenInterface SNT; // Total SNT in circulation - uint total; + uint public total; // Parameter to calculate Max SNT any one DApp can stake - uint ceiling; + uint public ceiling; // The max amount of tokens it is possible to stake, as a percentage of the total in circulation - uint max; + uint public max; // Decimal precision for this contract - uint decimals; + uint public decimals; // Prevents overflows in votes_minted - uint safeMax; + uint public safeMax; // Whether we need more than an id param to identify arbitrary data must still be discussed. struct Data { @@ -80,7 +80,7 @@ contract DAppStore is ApproveAndCallFallBack, BancorFormula { dapps.length++; Data storage d = dapps[dappIdx]; - d.developer = msg.sender; + d.developer = _from; d.id = _id; uint precision; @@ -265,12 +265,11 @@ contract DAppStore is ApproveAndCallFallBack, BancorFormula { require(_amount == amount, "Wrong amount"); - // TODO: check these function sigs! - if(sig == bytes4(0xe4bb1695)) { + if(sig == bytes4(0x1a214f43)) { _createDApp(_from, id, amount); - } else if(sig == bytes4(0xfaa5fd03)) { + } else if(sig == bytes4(0x466055f3)) { _downvote(_from, id); - } else if(sig == bytes4(0x0643e21b)) { + } else if(sig == bytes4(0x2b3df690)) { _upvote(_from, id, amount); } else { revert("Wrong method selector"); diff --git a/contracts/token/Controlled.sol b/contracts/token/Controlled.sol new file mode 100644 index 0000000..2e13ead --- /dev/null +++ b/contracts/token/Controlled.sol @@ -0,0 +1,22 @@ +pragma solidity ^0.5.1; + +contract Controlled { + /// @notice The address of the controller is the only address that can call + /// a function with this modifier + modifier onlyController { + require(msg.sender == controller, "Unauthorized"); + _; + } + + address payable public controller; + + constructor() internal { + controller = msg.sender; + } + + /// @notice Changes the controller of the contract + /// @param _newController The new controller of the contract + function changeController(address payable _newController) public onlyController { + controller = _newController; + } +} \ No newline at end of file diff --git a/contracts/token/MiniMeToken.sol b/contracts/token/MiniMeToken.sol new file mode 100644 index 0000000..dac8388 --- /dev/null +++ b/contracts/token/MiniMeToken.sol @@ -0,0 +1,634 @@ +pragma solidity ^0.5.1; + +/* + Copyright 2016, Jordi Baylina + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ +/** + * @title MiniMeToken Contract + * @author Jordi Baylina + * @dev This token contract's goal is to make it easy for anyone to clone this + * token using the token distribution at a given block, this will allow DAO's + * and DApps to upgrade their features in a decentralized manner without + * affecting the original token + * @dev It is ERC20 compliant, but still needs to under go further testing. + */ + +import "./Controlled.sol"; +import "./TokenController.sol"; +import "./ApproveAndCallFallBack.sol"; +import "./MiniMeTokenInterface.sol"; +import "./TokenFactory.sol"; + +/** + * @dev The actual token contract, the default controller is the msg.sender + * that deploys the contract, so usually this token will be deployed by a + * token controller contract, which Giveth will call a "Campaign" + */ +contract MiniMeToken is MiniMeTokenInterface, Controlled { + + string public name; //The Token's name: e.g. DigixDAO Tokens + uint8 public decimals; //Number of decimals of the smallest unit + string public symbol; //An identifier: e.g. REP + string public version = "MMT_0.1"; //An arbitrary versioning scheme + + /** + * @dev `Checkpoint` is the structure that attaches a block number to a + * given value, the block number attached is the one that last changed the + * value + */ + struct Checkpoint { + + // `fromBlock` is the block number that the value was generated from + uint128 fromBlock; + + // `value` is the amount of tokens at a specific block number + uint128 value; + } + + // `parentToken` is the Token address that was cloned to produce this token; + // it will be 0x0 for a token that was not cloned + MiniMeToken public parentToken; + + // `parentSnapShotBlock` is the block number from the Parent Token that was + // used to determine the initial distribution of the Clone Token + uint public parentSnapShotBlock; + + // `creationBlock` is the block number that the Clone Token was created + uint public creationBlock; + + // `balances` is the map that tracks the balance of each address, in this + // contract when the balance changes the block number that the change + // occurred is also included in the map + mapping (address => Checkpoint[]) balances; + + // `allowed` tracks any extra transfer rights as in all ERC20 tokens + mapping (address => mapping (address => uint256)) allowed; + + // Tracks the history of the `totalSupply` of the token + Checkpoint[] totalSupplyHistory; + + // Flag that determines if the token is transferable or not. + bool public transfersEnabled; + + // The factory used to create new clone tokens + TokenFactory public tokenFactory; + +//////////////// +// Constructor +//////////////// + + /** + * @notice Constructor to create a MiniMeToken + * @param _tokenFactory The address of the MiniMeTokenFactory contract that + * will create the Clone token contracts, the token factory needs to be + * deployed first + * @param _parentToken Address of the parent token, set to 0x0 if it is a + * new token + * @param _parentSnapShotBlock Block of the parent token that will + * determine the initial distribution of the clone token, set to 0 if it + * is a new token + * @param _tokenName Name of the new token + * @param _decimalUnits Number of decimals of the new token + * @param _tokenSymbol Token Symbol for the new token + * @param _transfersEnabled If true, tokens will be able to be transferred + */ + constructor( + address _tokenFactory, + address _parentToken, + uint _parentSnapShotBlock, + string memory _tokenName, + uint8 _decimalUnits, + string memory _tokenSymbol, + bool _transfersEnabled + ) + public + { + tokenFactory = TokenFactory(_tokenFactory); + name = _tokenName; // Set the name + decimals = _decimalUnits; // Set the decimals + symbol = _tokenSymbol; // Set the symbol + parentToken = MiniMeToken(address(uint160(_parentToken))); + parentSnapShotBlock = _parentSnapShotBlock; + transfersEnabled = _transfersEnabled; + creationBlock = block.number; + } + + +/////////////////// +// ERC20 Methods +/////////////////// + + /** + * @notice Send `_amount` tokens to `_to` from `msg.sender` + * @param _to The address of the recipient + * @param _amount The amount of tokens to be transferred + * @return Whether the transfer was successful or not + */ + function transfer(address _to, uint256 _amount) public returns (bool success) { + require(transfersEnabled); + return doTransfer(msg.sender, _to, _amount); + } + + /** + * @notice Send `_amount` tokens to `_to` from `_from` on the condition it + * is approved by `_from` + * @param _from The address holding the tokens being transferred + * @param _to The address of the recipient + * @param _amount The amount of tokens to be transferred + * @return True if the transfer was successful + */ + function transferFrom( + address _from, + address _to, + uint256 _amount + ) + public + returns (bool success) + { + + // The controller of this contract can move tokens around at will, + // this is important to recognize! Confirm that you trust the + // controller of this contract, which in most situations should be + // another open source smart contract or 0x0 + if (msg.sender != controller) { + require(transfersEnabled); + + // The standard ERC 20 transferFrom functionality + if (allowed[_from][msg.sender] < _amount) { + return false; + } + allowed[_from][msg.sender] -= _amount; + } + return doTransfer(_from, _to, _amount); + } + + /** + * @dev This is the actual transfer function in the token contract, it can + * only be called by other functions in this contract. + * @param _from The address holding the tokens being transferred + * @param _to The address of the recipient + * @param _amount The amount of tokens to be transferred + * @return True if the transfer was successful + */ + function doTransfer( + address _from, + address _to, + uint _amount + ) + internal + returns(bool) + { + + if (_amount == 0) { + return true; + } + + require(parentSnapShotBlock < block.number); + + // Do not allow transfer to 0x0 or the token contract itself + require((_to != address(0)) && (_to != address(this))); + + // If the amount being transfered is more than the balance of the + // account the transfer returns false + uint256 previousBalanceFrom = balanceOfAt(_from, block.number); + if (previousBalanceFrom < _amount) { + return false; + } + + // Alerts the token controller of the transfer + if (isContract(controller)) { + require(TokenController(controller).onTransfer(_from, _to, _amount)); + } + + // First update the balance array with the new value for the address + // sending the tokens + updateValueAtNow(balances[_from], previousBalanceFrom - _amount); + + // Then update the balance array with the new value for the address + // receiving the tokens + uint256 previousBalanceTo = balanceOfAt(_to, block.number); + require(previousBalanceTo + _amount >= previousBalanceTo); // Check for overflow + updateValueAtNow(balances[_to], previousBalanceTo + _amount); + + // An event to make the transfer easy to find on the blockchain + emit Transfer(_from, _to, _amount); + + return true; + } + + function doApprove( + address _from, + address _spender, + uint256 _amount + ) + internal + returns (bool) + { + require(transfersEnabled); + + // To change the approve amount you first have to reduce the addresses` + // allowance to zero by calling `approve(_spender,0)` if it is not + // already 0 to mitigate the race condition described here: + // https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + require((_amount == 0) || (allowed[_from][_spender] == 0)); + + // Alerts the token controller of the approve function call + if (isContract(controller)) { + require(TokenController(controller).onApprove(_from, _spender, _amount)); + } + + allowed[_from][_spender] = _amount; + emit Approval(_from, _spender, _amount); + return true; + } + + /** + * @param _owner The address that's balance is being requested + * @return The balance of `_owner` at the current block + */ + function balanceOf(address _owner) external view returns (uint256 balance) { + return balanceOfAt(_owner, block.number); + } + + /** + * @notice `msg.sender` approves `_spender` to spend `_amount` tokens on + * its behalf. This is a modified version of the ERC20 approve function + * to be a little bit safer + * @param _spender The address of the account able to transfer the tokens + * @param _amount The amount of tokens to be approved for transfer + * @return True if the approval was successful + */ + function approve(address _spender, uint256 _amount) external returns (bool success) { + doApprove(msg.sender, _spender, _amount); + } + + /** + * @dev This function makes it easy to read the `allowed[]` map + * @param _owner The address of the account that owns the token + * @param _spender The address of the account able to transfer the tokens + * @return Amount of remaining tokens of _owner that _spender is allowed + * to spend + */ + function allowance( + address _owner, + address _spender + ) + external + view + returns (uint256 remaining) + { + return allowed[_owner][_spender]; + } + /** + * @notice `msg.sender` approves `_spender` to send `_amount` tokens on + * its behalf, and then a function is triggered in the contract that is + * being approved, `_spender`. This allows users to use their tokens to + * interact with contracts in one function call instead of two + * @param _spender The address of the contract able to transfer the tokens + * @param _amount The amount of tokens to be approved for transfer + * @return True if the function call was successful + */ + function approveAndCall( + address _spender, + uint256 _amount, + bytes memory _extraData + ) + public + returns (bool success) + { + require(doApprove(msg.sender, _spender, _amount)); + + ApproveAndCallFallBack(_spender).receiveApproval( + msg.sender, + _amount, + address(this), + _extraData + ); + + return true; + } + + /** + * @dev This function makes it easy to get the total number of tokens + * @return The total number of tokens + */ + function totalSupply() external view returns (uint) { + return totalSupplyAt(block.number); + } + + +//////////////// +// Query balance and totalSupply in History +//////////////// + + /** + * @dev Queries the balance of `_owner` at a specific `_blockNumber` + * @param _owner The address from which the balance will be retrieved + * @param _blockNumber The block number when the balance is queried + * @return The balance at `_blockNumber` + */ + function balanceOfAt( + address _owner, + uint _blockNumber + ) + public + view + returns (uint) + { + + // These next few lines are used when the balance of the token is + // requested before a check point was ever created for this token, it + // requires that the `parentToken.balanceOfAt` be queried at the + // genesis block for that token as this contains initial balance of + // this token + if ((balances[_owner].length == 0) + || (balances[_owner][0].fromBlock > _blockNumber)) { + if (address(parentToken) != address(0)) { + return parentToken.balanceOfAt(_owner, min(_blockNumber, parentSnapShotBlock)); + } else { + // Has no parent + return 0; + } + + // This will return the expected balance during normal situations + } else { + return getValueAt(balances[_owner], _blockNumber); + } + } + + /** + * @notice Total amount of tokens at a specific `_blockNumber`. + * @param _blockNumber The block number when the totalSupply is queried + * @return The total amount of tokens at `_blockNumber` + */ + function totalSupplyAt(uint _blockNumber) public view returns(uint) { + + // These next few lines are used when the totalSupply of the token is + // requested before a check point was ever created for this token, it + // requires that the `parentToken.totalSupplyAt` be queried at the + // genesis block for this token as that contains totalSupply of this + // token at this block number. + if ((totalSupplyHistory.length == 0) + || (totalSupplyHistory[0].fromBlock > _blockNumber)) { + if (address(parentToken) != address(0)) { + return parentToken.totalSupplyAt(min(_blockNumber, parentSnapShotBlock)); + } else { + return 0; + } + + // This will return the expected totalSupply during normal situations + } else { + return getValueAt(totalSupplyHistory, _blockNumber); + } + } + +//////////////// +// Clone Token Method +//////////////// + + /** + * @notice Creates a new clone token with the initial distribution being + * this token at `snapshotBlock` + * @param _cloneTokenName Name of the clone token + * @param _cloneDecimalUnits Number of decimals of the smallest unit + * @param _cloneTokenSymbol Symbol of the clone token + * @param _snapshotBlock Block when the distribution of the parent token is + * copied to set the initial distribution of the new clone token; + * if the block is zero than the actual block, the current block is used + * @param _transfersEnabled True if transfers are allowed in the clone + * @return The address of the new MiniMeToken Contract + */ + function createCloneToken( + string memory _cloneTokenName, + uint8 _cloneDecimalUnits, + string memory _cloneTokenSymbol, + uint _snapshotBlock, + bool _transfersEnabled + ) + public + returns(address) + { + uint snapshotBlock = _snapshotBlock; + if (snapshotBlock == 0) { + snapshotBlock = block.number; + } + MiniMeToken cloneToken = MiniMeToken(tokenFactory.createCloneToken( + address(this), + snapshotBlock, + _cloneTokenName, + _cloneDecimalUnits, + _cloneTokenSymbol, + _transfersEnabled + )); + + cloneToken.changeController(msg.sender); + + // An event to make the token easy to find on the blockchain + emit NewCloneToken(address(cloneToken), snapshotBlock); + return address(cloneToken); + } + +//////////////// +// Generate and destroy tokens +//////////////// + + /** + * @notice Generates `_amount` tokens that are assigned to `_owner` + * @param _owner The address that will be assigned the new tokens + * @param _amount The quantity of tokens generated + * @return True if the tokens are generated correctly + */ + function generateTokens( + address _owner, + uint _amount + ) + public + onlyController + returns (bool) + { + uint curTotalSupply = totalSupplyAt(block.number); + require(curTotalSupply + _amount >= curTotalSupply); // Check for overflow + uint previousBalanceTo = balanceOfAt(_owner, block.number); + require(previousBalanceTo + _amount >= previousBalanceTo); // Check for overflow + updateValueAtNow(totalSupplyHistory, curTotalSupply + _amount); + updateValueAtNow(balances[_owner], previousBalanceTo + _amount); + emit Transfer(address(0), _owner, _amount); + return true; + } + + /** + * @notice Burns `_amount` tokens from `_owner` + * @param _owner The address that will lose the tokens + * @param _amount The quantity of tokens to burn + * @return True if the tokens are burned correctly + */ + function destroyTokens( + address _owner, + uint _amount + ) + public + onlyController + returns (bool) + { + uint curTotalSupply = totalSupplyAt(block.number); + require(curTotalSupply >= _amount); + uint previousBalanceFrom = balanceOfAt(_owner, block.number); + require(previousBalanceFrom >= _amount); + updateValueAtNow(totalSupplyHistory, curTotalSupply - _amount); + updateValueAtNow(balances[_owner], previousBalanceFrom - _amount); + emit Transfer(_owner, address(0), _amount); + return true; + } + +//////////////// +// Enable tokens transfers +//////////////// + + /** + * @notice Enables token holders to transfer their tokens freely if true + * @param _transfersEnabled True if transfers are allowed in the clone + */ + function enableTransfers(bool _transfersEnabled) public onlyController { + transfersEnabled = _transfersEnabled; + } + +//////////////// +// Internal helper functions to query and set a value in a snapshot array +//////////////// + + /** + * @dev `getValueAt` retrieves the number of tokens at a given block number + * @param checkpoints The history of values being queried + * @param _block The block number to retrieve the value at + * @return The number of tokens being queried + */ + function getValueAt( + Checkpoint[] storage checkpoints, + uint _block + ) + internal + view + returns (uint) + { + if (checkpoints.length == 0) { + return 0; + } + + // Shortcut for the actual value + if (_block >= checkpoints[checkpoints.length-1].fromBlock) { + return checkpoints[checkpoints.length-1].value; + } + if (_block < checkpoints[0].fromBlock) { + return 0; + } + + // Binary search of the value in the array + uint min = 0; + uint max = checkpoints.length-1; + while (max > min) { + uint mid = (max + min + 1) / 2; + if (checkpoints[mid].fromBlock<=_block) { + min = mid; + } else { + max = mid-1; + } + } + return checkpoints[min].value; + } + + /** + * @dev `updateValueAtNow` used to update the `balances` map and the + * `totalSupplyHistory` + * @param checkpoints The history of data being updated + * @param _value The new number of tokens + */ + function updateValueAtNow(Checkpoint[] storage checkpoints, uint _value) internal { + if ((checkpoints.length == 0) + || (checkpoints[checkpoints.length -1].fromBlock < block.number)) { + Checkpoint storage newCheckPoint = checkpoints[checkpoints.length++]; + newCheckPoint.fromBlock = uint128(block.number); + newCheckPoint.value = uint128(_value); + } else { + Checkpoint storage oldCheckPoint = checkpoints[checkpoints.length-1]; + oldCheckPoint.value = uint128(_value); + } + } + + /** + * @dev Internal function to determine if an address is a contract + * @param _addr The address being queried + * @return True if `_addr` is a contract + */ + function isContract(address _addr) internal view returns(bool) { + uint size; + if (_addr == address(0)){ + return false; + } + assembly { + size := extcodesize(_addr) + } + return size>0; + } + + /** + * @dev Helper function to return a min betwen the two uints + */ + function min(uint a, uint b) internal pure returns (uint) { + return a < b ? a : b; + } + + /** + * @notice The fallback function: If the contract's controller has not been + * set to 0, then the `proxyPayment` method is called which relays the + * ether and creates tokens as described in the token controller contract + */ + function () external payable { + require(isContract(controller)); + require(TokenController(controller).proxyPayment.value(msg.value)(msg.sender)); + } + +////////// +// Safety Methods +////////// + + /** + * @notice This method can be used by the controller to extract mistakenly + * sent tokens to this contract. + * @param _token The address of the token contract that you want to recover + * set to 0 in case you want to extract ether. + */ + function claimTokens(address _token) public onlyController { + if (_token == address(0)) { + controller.transfer(address(this).balance); + return; + } + + MiniMeToken token = MiniMeToken(address(uint160(_token))); + uint balance = token.balanceOf(address(this)); + token.transfer(controller, balance); + emit ClaimedTokens(_token, controller, balance); + } + +//////////////// +// Events +//////////////// + event ClaimedTokens(address indexed _token, address indexed _controller, uint _amount); + event Transfer(address indexed _from, address indexed _to, uint256 _amount); + event NewCloneToken(address indexed _cloneToken, uint snapshotBlock); + event Approval( + address indexed _owner, + address indexed _spender, + uint256 _amount + ); + +} \ No newline at end of file diff --git a/contracts/token/MiniMeTokenFactory.sol b/contracts/token/MiniMeTokenFactory.sol new file mode 100644 index 0000000..152da69 --- /dev/null +++ b/contracts/token/MiniMeTokenFactory.sol @@ -0,0 +1,50 @@ +pragma solidity ^0.5.1; + +import "./TokenFactory.sol"; +import "./MiniMeToken.sol"; + +//////////////// +// MiniMeTokenFactory +//////////////// + +/** + * @dev This contract is used to generate clone contracts from a contract. + * In solidity this is the way to create a contract from a contract of the + * same class + */ +contract MiniMeTokenFactory is TokenFactory { + + /** + * @notice Update the DApp by creating a new token with new functionalities + * the msg.sender becomes the controller of this clone token + * @param _parentToken Address of the token being cloned + * @param _snapshotBlock Block of the parent token that will + * determine the initial distribution of the clone token + * @param _tokenName Name of the new token + * @param _decimalUnits Number of decimals of the new token + * @param _tokenSymbol Token Symbol for the new token + * @param _transfersEnabled If true, tokens will be able to be transferred + * @return The address of the new token contract + */ + function createCloneToken( + address _parentToken, + uint _snapshotBlock, + string memory _tokenName, + uint8 _decimalUnits, + string memory _tokenSymbol, + bool _transfersEnabled + ) public returns (address payable) { + MiniMeToken newToken = new MiniMeToken( + address(this), + _parentToken, + _snapshotBlock, + _tokenName, + _decimalUnits, + _tokenSymbol, + _transfersEnabled + ); + + newToken.changeController(msg.sender); + return address(newToken); + } +} \ No newline at end of file diff --git a/contracts/token/TokenController.sol b/contracts/token/TokenController.sol new file mode 100644 index 0000000..409da5b --- /dev/null +++ b/contracts/token/TokenController.sol @@ -0,0 +1,33 @@ +pragma solidity ^0.5.0; +/** + * @dev The token controller contract must implement these functions + */ +interface TokenController { + /** + * @notice Called when `_owner` sends ether to the MiniMe Token contract + * @param _owner The address that sent the ether to create tokens + * @return True if the ether is accepted, false if it throws + */ + function proxyPayment(address _owner) external payable returns(bool); + + /** + * @notice Notifies the controller about a token transfer allowing the + * controller to react if desired + * @param _from The origin of the transfer + * @param _to The destination of the transfer + * @param _amount The amount of the transfer + * @return False if the controller does not authorize the transfer + */ + function onTransfer(address _from, address _to, uint _amount) external returns(bool); + + /** + * @notice Notifies the controller about an approval allowing the + * controller to react if desired + * @param _owner The address that calls `approve()` + * @param _spender The spender in the `approve()` call + * @param _amount The amount in the `approve()` call + * @return False if the controller does not authorize the approval + */ + function onApprove(address _owner, address _spender, uint _amount) external + returns(bool); +} \ No newline at end of file diff --git a/contracts/token/TokenFactory.sol b/contracts/token/TokenFactory.sol new file mode 100644 index 0000000..c5ddceb --- /dev/null +++ b/contracts/token/TokenFactory.sol @@ -0,0 +1,12 @@ +pragma solidity ^0.5.1; + +contract TokenFactory { + function createCloneToken( + address _parentToken, + uint _snapshotBlock, + string memory _tokenName, + uint8 _decimalUnits, + string memory _tokenSymbol, + bool _transfersEnabled + ) public returns (address payable); +} diff --git a/test/contract_spec.js b/test/contract_spec.js deleted file mode 100644 index 71d999f..0000000 --- a/test/contract_spec.js +++ /dev/null @@ -1,43 +0,0 @@ -// /*global contract, config, it, assert*/ -/* -const SimpleStorage = require('Embark/contracts/SimpleStorage'); - -let accounts; - -// For documentation please see https://embark.status.im/docs/contracts_testing.html -config({ - //deployment: { - // accounts: [ - // // you can configure custom accounts with a custom balance - // // see https://embark.status.im/docs/contracts_testing.html#Configuring-accounts - // ] - //}, - contracts: { - "SimpleStorage": { - args: [100] - } - } -}, (_err, web3_accounts) => { - accounts = web3_accounts -}); - -contract("SimpleStorage", function () { - this.timeout(0); - - it("should set constructor value", async function () { - let result = await SimpleStorage.methods.storedData().call(); - assert.strictEqual(parseInt(result, 10), 100); - }); - - it("set storage value", async function () { - await SimpleStorage.methods.set(150).send(); - let result = await SimpleStorage.methods.get().call(); - assert.strictEqual(parseInt(result, 10), 150); - }); - - it("should have account with balance", async function() { - let balance = await web3.eth.getBalance(accounts[0]); - assert.ok(parseInt(balance, 10) > 0); - }); -} -*/ diff --git a/test/dappstore_spec.js b/test/dappstore_spec.js new file mode 100644 index 0000000..912d73d --- /dev/null +++ b/test/dappstore_spec.js @@ -0,0 +1,84 @@ +/*global contract, config, it, embark, web3, before, describe, beforeEach*/ +const TestUtils = require("../utils/testUtils"); + +const DAppStore = require('Embark/contracts/DAppStore'); +const SNT = embark.require('Embark/contracts/SNT'); + + +config({ + deployment: { + accounts: [ + { + mnemonic: "foster gesture flock merge beach plate dish view friend leave drink valley shield list enemy", + balance: "5 ether", + numAddresses: "10" + } + ] + }, + contracts: { + "MiniMeToken": { "deploy": false }, + "MiniMeTokenFactory": { }, + "SNT": { + "instanceOf": "MiniMeToken", + "args": [ + "$MiniMeTokenFactory", + "0x0000000000000000000000000000000000000000", + 0, + "TestMiniMeToken", + 18, + "SNT", + true + ] + }, + "DAppStore": { + args: [ "$SNT" ] + } + } +}, (_err, web3_accounts) => { + accounts = web3_accounts +}); + +contract("DAppStore", function () { + + this.timeout(0); + + it("should set max and safeMax values correctly", async function () { + let resultMax = await DAppStore.methods.max().call(); + let resultSafeMax = await DAppStore.methods.safeMax().call(); + let expectedMax = Math.round(3470483788 * 588 / 1000000); + let expectedSafeMax = Math.round(expectedMax * 0.98); + assert.strictEqual(parseInt(resultMax, 10), expectedMax); + assert.strictEqual(parseInt(resultSafeMax, 10), expectedSafeMax); + }); + + it("should create a new DApp and initialise it correctly", async function () { + let id = "0x7465737400000000000000000000000000000000000000000000000000000000"; + let amount = 10000; + + await SNT.methods.generateTokens(accounts[0], amount).send(); + const encodedCall = DAppStore.methods.createDApp(id,amount).encodeABI(); + await SNT.methods.approveAndCall(DAppStore.options.address, amount, encodedCall).send({from: accounts[0]}); + + let receipt = await DAppStore.methods.dapps(0).call(); + + let developer = accounts[0]; + assert.strictEqual(receipt.developer, developer); + + assert.strictEqual(receipt.id, id); + assert.strictEqual(parseInt(receipt.balance, 10), amount); + + let max = await DAppStore.methods.max().call(); + let decimals = await DAppStore.methods.decimals().call(); + let rate = Math.round(decimals - (amount * decimals/max)); + assert.strictEqual(parseInt(receipt.rate, 10), rate); + + let available = amount * rate; + assert.strictEqual(parseInt(receipt.available, 10), available); + + let votes_minted = Math.round((available/decimals) ** (decimals/rate)); + assert.strictEqual(parseInt(receipt.votes_minted, 10), votes_minted); + + assert.strictEqual(parseInt(receipt.votes_cast, 10), 0); + assert.strictEqual(parseInt(receipt.effective_balance, 10), amount); + }) +}); diff --git a/utils/testUtils.js b/utils/testUtils.js new file mode 100644 index 0000000..f40d418 --- /dev/null +++ b/utils/testUtils.js @@ -0,0 +1,147 @@ +/*global assert, web3*/ + +// This has been tested with the real Ethereum network and Testrpc. +// Copied and edited from: https://gist.github.com/xavierlepretre/d5583222fde52ddfbc58b7cfa0d2d0a9 +exports.assertReverts = (contractMethodCall, maxGasAvailable) => { + return new Promise((resolve, reject) => { + try { + resolve(contractMethodCall()); + } catch (error) { + reject(error); + } + }) + .then(tx => { + assert.equal(tx.receipt.gasUsed, maxGasAvailable, "tx successful, the max gas available was not consumed"); + }) + .catch(error => { + if ((String(error)).indexOf("invalid opcode") < 0 && (String(error)).indexOf("out of gas") < 0) { + // Checks if the error is from TestRpc. If it is then ignore it. + // Otherwise relay/throw the error produced by the above assertion. + // Note that no error is thrown when using a real Ethereum network AND the assertion above is true. + throw error; + } + }); + }; + + exports.listenForEvent = event => new Promise((resolve, reject) => { + event({}, (error, response) => { + if (!error) { + resolve(response.args); + } else { + reject(error); + } + event.stopWatching(); + }); + }); + + exports.eventValues = (receipt, eventName) => { + if (receipt.events[eventName]) return receipt.events[eventName].returnValues; + }; + + exports.addressToBytes32 = (address) => { + const stringed = "0000000000000000000000000000000000000000000000000000000000000000" + address.slice(2); + return "0x" + stringed.substring(stringed.length - 64, stringed.length); + }; + + // OpenZeppelin's expectThrow helper - + // Source: https://github.com/OpenZeppelin/zeppelin-solidity/blob/master/test/helpers/expectThrow.js + exports.expectThrow = async promise => { + try { + await promise; + } catch (error) { + // TODO: Check jump destination to destinguish between a throw + // and an actual invalid jump. + const invalidOpcode = error.message.search('invalid opcode') >= 0; + // TODO: When we contract A calls contract B, and B throws, instead + // of an 'invalid jump', we get an 'out of gas' error. How do + // we distinguish this from an actual out of gas event? (The + // testrpc log actually show an 'invalid jump' event.) + const outOfGas = error.message.search('out of gas') >= 0; + const revert = error.message.search('revert') >= 0; + assert( + invalidOpcode || outOfGas || revert, + 'Expected throw, got \'' + error + '\' instead', + ); + return; + } + assert.fail('Expected throw not received'); + }; + + + exports.assertJump = (error) => { + assert(error.message.search('VM Exception while processing transaction: revert') > -1, 'Revert should happen'); + }; + + + function callbackToResolve(resolve, reject) { + return function(error, value) { + if (error) { + reject(error); + } else { + resolve(value); + } + }; + } + + exports.promisify = (func) => + (...args) => { + return new Promise((resolve, reject) => { + const callback = (err, data) => err ? reject(err) : resolve(data); + func.apply(this, [...args, callback]); + }); + }; + + exports.zeroAddress = '0x0000000000000000000000000000000000000000'; + exports.zeroBytes32 = "0x0000000000000000000000000000000000000000000000000000000000000000"; + exports.timeUnits = { + seconds: 1, + minutes: 60, + hours: 60 * 60, + days: 24 * 60 * 60, + weeks: 7 * 24 * 60 * 60, + years: 365 * 24 * 60 * 60 + }; + + exports.ensureException = function(error) { + assert(isException(error), error.toString()); + }; + + function isException(error) { + let strError = error.toString(); + return strError.includes('invalid opcode') || strError.includes('invalid JUMP') || strError.includes('revert'); + } + + const evmMethod = (method, params = []) => { + return new Promise(function(resolve, reject) { + const sendMethod = (web3.currentProvider.sendAsync) ? web3.currentProvider.sendAsync.bind(web3.currentProvider) : web3.currentProvider.send.bind(web3.currentProvider); + sendMethod( + { + jsonrpc: '2.0', + method, + params, + id: new Date().getSeconds() + }, + (error, res) => { + if (error) { + return reject(error); + } + resolve(res.result); + } + ); + }); + }; + + exports.evmSnapshot = async () => { + const result = await evmMethod("evm_snapshot"); + return web3.utils.hexToNumber(result); + }; + + exports.evmRevert = (id) => { + const params = [id]; + return evmMethod("evm_revert", params); + }; + + exports.increaseTime = async (amount) => { + await evmMethod("evm_increaseTime", [Number(amount)]); + await evmMethod("evm_mine"); + }; \ No newline at end of file