@mybit/contracts

0.1.38 • Public • Published

MyBit Logo

MyBit Logo MyBit Developer Portal > Contracts

MyBit Network - Contract SDK

CircleCI Coverage Status

A software development kit for the automated machine economy.

The contract SDK is a set of contracts that implement the business logic for the network. The Network SDK is a tool for building Wealth Management Decentralised Applications, without needing in depth blockchain knowledge. For a simple example you can see Hello-Network. To quickly integrate the SDK contracts see Network.js.

Getting Started

First install dependencies using Yarn:

yarn

If successful you should see output similar to below:

[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
Done in 5.69s.

Testing

To start local blockchain, Ganache run:

yarn blockchain

In another terminal window, you can now run the tests:

yarn tests

To see code-coverage run:

yarn coverage

Deploying the MyBit Platform

Setup Web3 in Truffle

Our truffle.js file is setup to provide web3 via HDWalletProvider. We have a mnemonic in a json file labelled mnemonic.json. It has the following format:

{
  "mnemonic": "your mnemonic here",
  "infura": "your infura key here"
}

However, if you prefer not to rely on a mnemonic you will have to setup a custom truffle.js file to handle your web3 provider.

Set Owners

In the migrations folder, go to (2_deploy_databases.js)[migrations/2_deploy_databases.js] and find the line that sets let OWNERS. You'll want to change that value for the network you will be deploying on. The OWNERS value is an array of all the Ethereum addresses that have the right to add, remove, or update contracts on the platform and call special functions that set values to the database. For the deployment, you'll want one of the addresses to be the account you are deploying from (i.e. accounts[0]). You can remove that account after deployment is complete.

Set Platform Values

Also in migrations is a file called (6_deploy_platform.js)[migrations/6_deploy_platform.js]. There are several variables you will need to set to deploy a platform that fits your needs.

  • FUNDS_WALLET The wallet where platform fees paid by investors will go.
  • ASSETS_WALLET The wallet where a percentage of asset tokens for the platform will go. This can be the same wallet as FUNDS_WALLET.
  • PLATFORM_FEE The percentage of an investors payment that goes to the platform.
  • PLATFORM_PERCENTAGE The percentage of an asset that goes to the platform.
  • PLATFORM_TOKEN The token that is used to hold an asset manager's collateral. This token should be burnable and available on Kyber to make use of all our contracts. Some functions will fail if it is not.
  • BASE_COLLATERAL The base percentage that all asset managers must put up as collateral regardless of the number of successful crowdsales they've completed.
  • LOW_COLLATERAL Extra collateral for asset managers that have completed 0-4 crowdsales
  • MID_COLLATERAL Extra collateral for asset managers that have completed 5-9 crowdsales
  • HIGH_COLLATERAL Extra collateral for asset managers that have completed 10-24 crowdsales

For collateral, generally you'd want the collateral percentage to reduce as the asset managers manage more assets.

Deploy Platform

Once these values are set, you simply call truffle migrate for the particular network you'd like to deploy on. e.g.:

truffle migrate --network ropsten

Deployed Contracts

Mainnet

MyBitToken 0x5d60d8d7eF6d37E16EBABc324de3bE57f135e0BC

Database 0x5FCebEB70b88e86dD880352684e775B0F4D57C71

Events 0xeB6533f29A54C2c18BB2Ce2a100dE717692A518F

ContractManager 0xDAe939bBa0DC30695B3cBa66e16B9a6c2B15B7d3

API 0x4250f3d2afec30752ce7b99d9a6b276d9d6417e1

MultiOwned 0x9Cb221402a45f3c2561D0634A56A3F3411e0805D

Pausible 0xb7DEe440fB88180B10c8104fE0587DD083914151

CrowdsaleReserve 0x4D10da0724D5DBaC5015b22D917e79a882f4E514

EscrowReserve 0x05C9fAA268CBc9aB9AF0ef844a208c28D0ad5aCf

AssetManagerEscrow 0x9aB994054db1f001e8662A83A36E77Bc27486f7A

AssetManagerFunds 0x042422bb2d8493e068684e4f880463bce6215ac8

Platform 0x9C05dc7c7f6332CBc16Ce1e196Ff6928f3fCe5bF

Operators 0xeC841C878435Ba4f28BF305a00C1483dB0D96A20

Minter 0x1405Ca0c8A3b5196A6A3365E971BfD9A8AdA500C

CrowdsaleGeneratorETH 0xcA12697245B24ee09D97678f9A06997751dE50BE

CrowdsaleGeneratorERC20 0xB855e1eFBD428b2253ED65C3c830bC55254E48c0

CrowdsaleETH 0x3c4f8ea7531fc7f0693816ea6889f7d1343c36da

CrowdsaleERC20 0x1056d0c770a7614cee6b2f1b3935866f3284ddf8

AssetGenerator 0x59F67BDf80bAaBACd90967D35Ff2bc690E02d7F9

MiniMeTokenFactory 0x3CeFefCbD7A6Cd0dfd1f244922A2932a00A233E6

Ropsten

MyBitToken 0xC68D7C356e1b725F75cBaf1306A2603abd7157CA

Database 0xe078984b0F02054Fa7514178f2541f08657Feb3e

Events 0xEcA758673c1940425196FEd1DaE7eb7a1F442273

ContractManager 0xf7D7545d7291A3082c72961Ef91395834BEd0202

API 0x7d2e69b7d538ee8084315a9c187ca6932b3cd0b4

MultiOwned 0xe8F982F52dd9a493966BF2D474A94B922C56CA2D

Pausible 0x3EefF15E4b0F661B873E62A0B466BEC6a0E3ef86

Platform 0xAee283f8f2a9EB65826042F44180616343D6DF4c

Operators 0x15c62ee52d42ad5272c24cd1392f8618e4318eb3

AssetManagerEscrow 0xD6B42238faEbE197721934500612D5f535D57d94

AssetManagerFunds 0x62d2077bfd26890ef7b738b5dbadc2baf621f74e

AssetGenerator 0x2234b17aFFcaA806b90c539270Fc6e0270625ed2

CrowdsaleETH 0x830cba449d49b7742ae0ee6d7640eed39e689ff6

CrowdsaleGeneratorETH 0x3aE1Ec68512D5FbC3586918D59ddfE415713C241

CrowdsaleERC20 0x2e7c056aab35d1d728dd1e026d0e8010caf0d5ec

CrowdsaleGeneratorERC20 0xD361Dc2b0F0fE4EE92d6509Ffe44395DE40d332d

CrowdsaleReserve 0xee7A234DEBCfBD72DD94d7D9a38237E1A4d42274

EscrowReserve 0xC667949817C0b5909a751dcC178E164E078445c8

Minter 0xf8dbA4d512524596612BFbf44cCfB81aa41FD48f

MiniMeTokenFactory 0x5139f54527A782BD39DB7aFeF1B1322DbF994Ad8

Rinkeby

MyBitToken 0xE46A97127504f98a9C1d6F92689E3B50C81a1164

Database 0xcd5099a7254089677B8DcE964F48188e49d36d6E

Events 0xa9cd60b53A535507ABEDa25de9267eD614660A38

ContractManager 0xa89188CaE0636b55C5c1d9335667F14759237dD8

API 0xE42848b7fd2520eC47116275502C3F30c689d163

MultiOwned 0x034405aCf7D768b33907A7C643aA04eFB4A5287f

Pausible 0x2A6906518199D8694715b03d603BaDc5a6E8CE13

CrowdsaleReserve 0x6c2a5EB3409fbe5C247364ab274B54c207e35c17

EscrowReserve 0x8C46718f532230a7Cb5B85Dc17beCDfB6Df77170

AssetManagerEscrow 0x779D29CF456f8A3B569a731EB95FdC220a0fAC75

AssetManagerFunds 0x947879c5e40c36F575690AB7a33F636bd4902cCA

Platform 0x31418128196d17bec88ebAc1cf9c46b19fbA67A5

MiniMeTokenFactory 0x710345A8310C7D82890b5D501b0Af148962E4053

Operators 0x506B08245C8B90D174c8b635aA00c9DC08883175

Minter 0xF0BD0F90631D5e211D645e6CA1C34a3Cd9AC6304

CrowdsaleGeneratorETH 0x72C90D984e03ef21aa9590762F02f9E7A7306b75

CrowdsaleGeneratorERC20 0x79dF292191c9C0A9c0A3E2C16ac515ebd600E375

CrowdsaleETH 0xDbC31a2d63D5223Bc705243e5a4d436bd08F2d5b

CrowdsaleERC20 0x64fBcADD8D3d1B7e87cb340e30474BD5742341bc

AssetGenerator 0x2DdEb014D5148fC87ddb7e014848bF991Ed1961d

Roles

There are generally 4 different roles on the platform. The Investor, the AssetManager, the Operator, and the PlatformOwners. Investors can contribute ETH or Erc20 tokens to invest in new asset crowdsales. The continued functioning of the asset is ensured by the AssetManager, who receives a fee for his work and escrows tokens as collateral to investors. The Operator receives funds from the crowdsale and produces and installs the asset. PlatformOwners can choose how assets are governed, and whether or not a contract upgrade should happen. The platform owner can be a single account or a contract governed by many accounts.

Contract overview

Before creating assets, certain variables and parameters have to be set:

  • All contracts must be registered in ContractManager before writing to database
  • All users must approve the current contract state, which changes everytime a contract is added/updated in ContractManager
  • Users must approve ERC20Burner to burn platform tokens before using key functionality
  • Platform wallet and platform token must be set
  • Operators must be registered and choose which currencies they wish to accept

Basic functionality for these critical operations are outlined below.

All contracts are found here

Database

Contracts in the SDK store all long-term data in a database contract, which allows for contracts to be upgraded without losing valuable data. The Database stores all data using a bytes32 type, which is often the keccak256 hash of the variableName, ID, address that make up that variable.

The Database stores any data type under a bytes32 key:

mapping(bytes32 => uint) public uintStorage;
mapping(bytes32 => string) public stringStorage;
mapping(bytes32 => address) public addressStorage;
mapping(bytes32 => bytes) public bytesStorage;
mapping(bytes32 => bytes32) public bytes32Storage;
mapping(bytes32 => bool) public boolStorage;
mapping(bytes32 => int) public intStorage;

Storing an integer looks like this:

  database.setUint(keccak256(abi.encodePacked("fundingDeadline", assetID)), 20000000);

The Database stores this with key = sha3("fundingDeadline", assetID) and value = 20000000

function setUint(bytes32 _key, address _value)
onlyApprovedContract
external {
    uintStorage[_key] = _value;
}

Events

The events contract is used as a central store of all events that happen on the platform.

A contract may emit an event by calling a function in the events contract:

  events.asset('Asset funding started', _assetURI, assetAddress, msg.sender);

There are several event functions that emit different data types depending on their use case. One can filter using the event's messageID and origin which are indexed values that are shared by all event functions.

Events can be queried in javascript like so:

  await eventsContract.getPastEvents('LogAsset', {
                        filter: {
                          messageID: web3.utils.sha3('Asset funding started'),
                          origin: '0x00000000...' },
                        fromBlock: 0,
                        toBlock: 'latest'});

API

The API contract can be used to easily fetch variables from the database

  function getCrowdsaleDeadline(address _assetAddress)
  public
  view
  returns(uint) {
    return database.uintStorage(keccak256(abi.encodePacked("crowdsale.deadline", _assetAddress)));
  }

ContractManagement

The Database restricts write access to only contract that are on the platform

// Caller must be registered as a contract through ContractManager.sol
modifier onlyApprovedContract() {
    require(boolStorage[keccak256(abi.encodePacked("contract", msg.sender))]);
    _;
}

To give a contract write access to the database, you must call addContract(contractName, contractAddress) from a platform owner account:

  function addContract(string _name, address _contractAddress)
  external
  isTrue(_contractAddress != address(0))
  isTrue(bytes(_name).length != uint(0))
  anyOwner {
    require(!database.boolStorage(keccak256(abi.encodePacked("contract", _contractAddress))));
    require(database.addressStorage(keccak256(abi.encodePacked("contract", _name))) == address(0));
    database.setAddress(keccak256(abi.encodePacked("contract", _name)), _contractAddress);
    database.setBool(keccak256(abi.encodePacked("contract", _contractAddress)), true);
    bytes32 currentState = database.bytes32Storage(keccak256(abi.encodePacked("currentState")));    
    bytes32 newState = keccak256(abi.encodePacked(currentState, _contractAddress));
    database.setBytes32(keccak256(abi.encodePacked("currentState")), newState);
    emit LogContractAdded(_contractAddress, _name, block.number);
  }

Every time a contract is added or updated the contract state will change, requiring approval from users before they interact with the platform. Users can also choose to ignore future state changes. This can be done by calling the following function:

  function setContractStatePreferences(bool _acceptCurrentState, bool _ignoreStateChanges)
  external
  returns (bool) {
    bytes32 currentState = database.bytes32Storage(keccak256(abi.encodePacked("currentState")));
    database.setBool(keccak256(abi.encodePacked(currentState, msg.sender)), _acceptCurrentState);
    database.setBool(keccak256(abi.encodePacked("ignoreStateChanges", msg.sender)), _ignoreStateChanges);
    emit LogContractStatePreferenceChanged(msg.sender, _acceptCurrentState, _ignoreStateChanges);
    return true;
  }

Functions which directly effect the user in case of contract upgrades will use the acceptedState() modifier to prevent users from accidentally interacting with contracts that they haven't agreed to interact with.

modifier acceptedState(address _investor) {
  bytes32 currentState = database.bytes32Storage(keccak256(abi.encodePacked("currentState")));
  require(database.boolStorage(keccak256(abi.encodePacked(currentState, _investor))) || database.boolStorage(keccak256(abi.encodePacked("ignoreStateChanges", _investor))));
  _;
}

CrowdsaleReserve

To accomodate upgrading the crowdsale contract, all funds for ongoing crowdsales are stored inside the crowdsale reserve contract. Only the current crowdsale contracts have permission to use these functions.

To transfer funds from an investor to the reserve, the crowdsale contract calls this function (this will fail if the investor has not approved the reserve contract to transfer funds):

  function requestERC20(address _payer, uint256 _amount, address _tokenAddress) external returns (bool){
    require(msg.sender == database.addressStorage(keccak256(abi.encodePacked("contract", "CrowdsaleERC20"))), 'Contract not authorized');
    require(ERC20(_tokenAddress).transferFrom(_payer, address(this), _amount), 'Transfer failed');
    events.transaction("ERC20 received by crowdsale reserve", _payer, address(this), _amount, _tokenAddress);
  }

And to send funds the crowdsale contract must call this function:

  function issueERC20(address _receiver, uint256 _amount, address _tokenAddress) external returns (bool){
    require(msg.sender == database.addressStorage(keccak256(abi.encodePacked("contract", "CrowdsaleERC20"))), 'Contract not authorized');
    ERC20 erc20 = ERC20(_tokenAddress);
    require(erc20.balanceOf(this) >= _amount, 'Not enough funds');
    require(erc20.transfer(_receiver, _amount), 'Transfer failed');
    events.transaction("ERC20 withdrawn from crowdsale reserve", address(this), _receiver, _amount, _tokenAddress);
    return true;
  }

EscrowReserve

Much like the crowdsale reserve, the escrow reserve is used to store funds that are used as collateral by asset managers. Only AssetManagerEscrow, CrowdsaleGeneratorETH, and CrowdsaleGeneratorERC20 contracts are able to interact with this contract.

To transfer funds from an asset mangaer to the reserve, the crowdsale generator contract calls this function:

  function requestERC20(address _payer, uint256 _amount, address _tokenAddress) external returns (bool){
    require(msg.sender == database.addressStorage(keccak256(abi.encodePacked("contract", "AssetManagerEscrow"))) ||
            msg.sender == database.addressStorage(keccak256(abi.encodePacked("contract", "CrowdsaleGeneratorETH"))) ||
            msg.sender == database.addressStorage(keccak256(abi.encodePacked("contract", "CrowdsaleGeneratorERC20"))));
    require(BurnableERC20(_tokenAddress).transferFrom(_payer, address(this), _amount));
    events.transaction("ERC20 received by escrow reserve", _payer, address(this), _amount, _tokenAddress);
  }

To return funds, the AssetManagerEscrow contract can call:

  function issueERC20(address _receiver, uint256 _amount, address _tokenAddress) external returns (bool){
    require(msg.sender == database.addressStorage(keccak256(abi.encodePacked("contract", "AssetManagerEscrow"))));
    BurnableERC20 erc20 = BurnableERC20(_tokenAddress);
    require(erc20.balanceOf(this) >= _amount);
    require(erc20.transfer(_receiver, _amount));
    events.transaction("ERC20 withdrawn from escrow reserve", address(this), _receiver, _amount, _tokenAddress);
    return true;
  }

Minter

This contract controls the creation and minting of the asset tokens (which handle the distribution of income from assets).

The crowdsale generator contracts may create new tokens upon the creation of a new crowdsale:

  function cloneToken(string _uri, address _erc20Address) external returns (address asset) {
    require(msg.sender == database.addressStorage(keccak256(abi.encodePacked("contract", "CrowdsaleGeneratorERC20"))) ||
            msg.sender == database.addressStorage(keccak256(abi.encodePacked("contract", "CrowdsaleGeneratorETH"))) ||
            msg.sender == database.addressStorage(keccak256(abi.encodePacked("contract", "AssetGenerator"))) );
    Minter_MiniMeTokenFactory factory = Minter_MiniMeTokenFactory(database.addressStorage(keccak256(abi.encodePacked("platform.tokenFactory"))));
    asset = factory.createCloneToken(address(0), 0, _uri, uint8(18), _uri, true, _erc20Address);
    return asset;
  }

New tokens are minted by the crowdsale contracts:

  function mintAssetTokens(address _assetAddress, address _receiver, uint256 _amount) external returns (bool){
    require(msg.sender == database.addressStorage(keccak256(abi.encodePacked("contract", "CrowdsaleERC20"))) ||
            msg.sender == database.addressStorage(keccak256(abi.encodePacked("contract", "CrowdsaleETH"))) ||
            msg.sender == database.addressStorage(keccak256(abi.encodePacked("contract", "AssetGenerator"))) );
    require(Minter_MiniMeToken(_assetAddress).generateTokens(_receiver, _amount));
    return true;
  }

Control of the token can be passed to a DAO by the DAO deployer contract (which will allow the DAO to mint more tokens):

  function changeTokenController(address _assetAddress, address _newController) external returns (bool){
    require(msg.sender == database.addressStorage(keccak256(abi.encodePacked("contract", "DAODeployer"))));
    Minter_MiniMeToken(_assetAddress).changeController(_newController);
  }

However, depending on an investor vote, AssetManagerEscrow can also burn the asset manager's collateral:

  function burnERC20(uint256 _amount, address _tokenAddress) external returns (bool){
    require(msg.sender == database.addressStorage(keccak256(abi.encodePacked("contract", "AssetManagerEscrow"))));
    require(BurnableERC20(_tokenAddress).burn(_amount));
    events.transaction("ERC20 burnt by escrow reserve", address(this), address(0), _amount, _tokenAddress);
    return true;
  }

Platform-Variables

Before assets can be funded the platform owners must set the platform token, platform funds wallet, platform assets wallet, platform fee, platform percentage, token factory, and collateral levels by using:

  // @notice The token that the platform uses for holding collateral
  function setPlatformToken(address _tokenAddress)
  external
  onlyOwner {
    database.setAddress(keccak256(abi.encodePacked("platformToken")), _tokenAddress);
    emit LogPlatformToken(_tokenAddress);
  }

AND

  // @notice The wallet to receive payments here before initiating crowdsale
  function setPlatformFundsWallet(address _walletAddress)
  external
  onlyOwner {
    database.setAddress(keccak256(abi.encodePacked("platform.wallet.funds")), _walletAddress);
    //emit LogPlatformWallet(_walletAddress);
    events.registration('Platform funds wallet', _walletAddress);
  }

AND

  // @notice The wallet to receive asset tokens here before initiating crowdsale
  function setPlatformAssetsWallet(address _walletAddress)
  external
  onlyOwner {
    database.setAddress(keccak256(abi.encodePacked("platform.wallet.assets")), _walletAddress);
    events.registration('Platform assets wallet', _walletAddress);
  }  

AND

  // @notice The percentage of the payment that the platform receives when investors contribute
  function setPlatformFee(uint _percent)
  external
  onlyOwner {
    database.setUint(keccak256(abi.encodePacked("platform.fee")), _percent);
  }

AND

  // @notice The percentage of the asset tokens the platform receives from the crowdsale
  function setPlatformPercentage(uint _percent)
  external
  onlyOwner {
    database.setUint(keccak256(abi.encodePacked("platform.percentage")), _percent);
  }

AND

  // @notice Set the address of the token factory that clones the asset tokens
  function setTokenFactory(address _factory)
  external
  onlyOwner {
    database.setAddress(keccak256(abi.encodePacked("platform.tokenFactory")), _factory);
  }

AND

  // @notice Set the required collateral based on how many assets are managed by a user
  function setCollateralLevels(uint _base, uint _low, uint _mid, uint _high)
  external
  onlyOwner {
    database.setUint(keccak256(abi.encodePacked("collateral.base")), _base);
    for(uint i=0; i<5; i++){
      database.setUint(keccak256(abi.encodePacked("collateral.level", i)), _low);
    }
    for(i=5; i<10; i++){
      database.setUint(keccak256(abi.encodePacked("collateral.level", i)), _mid);
    }
    for(i=10; i<25; i++){
      database.setUint(keccak256(abi.encodePacked("collateral.level", i)), _high);
    }
  }

Onboarding Operators

The Operator must be registered, they must set what assets they operate, what currencies the assets take to purchase, and what currency they payout with. To set the operators you can call:

  function registerOperator(address _operatorAddress, string _operatorURI, string _ipfs, address _referrerAddress)
  external
  onlyOwner {
    require(_operatorAddress != address(0));
    bytes32 operatorID = keccak256(abi.encodePacked("operator.uri", _operatorURI));
    require(database.addressStorage(keccak256(abi.encodePacked("operator", operatorID))) == address(0));
    database.setBytes32(keccak256(abi.encodePacked("operator", _operatorAddress)), operatorID);
    database.setAddress(keccak256(abi.encodePacked("operator", operatorID)), _operatorAddress);
    database.setString(keccak256(abi.encodePacked("operator.ipfs", operatorID)), _ipfs);
    if(_referrerAddress == address(0)){
      database.setAddress(keccak256(abi.encodePacked("referrer", operatorID)), database.addressStorage(keccak256(abi.encodePacked("platform.wallet.assets"))));
    } else {
      database.setAddress(keccak256(abi.encodePacked("referrer", operatorID)), _referrerAddress);
    }

    events.operator('Operator registered', operatorID, _operatorURI, _ipfs, _operatorAddress);
  }

To add an asset (and set the initial currency):

  function addAsset(bytes32 _operatorID, string _name, string _ipfs, bool _acceptCrypto, bool _payoutCrypto, address _token)
  external
  onlyOperator(_operatorID)
  returns (bool) {
    bytes32 modelID = keccak256(abi.encodePacked('model.id', _operatorID, _name));
    require(database.addressStorage(keccak256(abi.encodePacked("model.operator", modelID))) == address(0));
    database.setAddress(keccak256(abi.encodePacked("model.operator", modelID)), msg.sender);
    database.setString(keccak256(abi.encodePacked('model.ipfs', modelID)), _ipfs);
    acceptToken(modelID, _token, _acceptCrypto);
    payoutToken(modelID, _token, _payoutCrypto);
    events.operator('Asset added', modelID, _name, _ipfs, msg.sender);
    return true;
  }

AND to set any other currencies:

  // @notice operator can choose which ERC20 tokens he's willing to accept as payment
  function acceptToken(bytes32 _modelID, address _tokenAddress, bool _accept)
  public
  onlyOperator(_modelID)
  returns (bool) {
    if(_tokenAddress == address(0)){
      database.setBool(keccak256(abi.encodePacked("model.acceptsEther", _modelID)), _accept);
    }
    database.setBool(keccak256(abi.encodePacked("model.acceptsToken", _modelID, _tokenAddress)), _accept);
    return true;
  }

AND/OR

// @notice operator can choose which ERC20 tokens it pays out with
  function payoutToken(bytes32 _modelID, address _tokenAddress, bool _payout)
  public
  onlyOperator(_modelID)
  returns (bool) {
    if(_tokenAddress == address(0)){
      database.setBool(keccak256(abi.encodePacked("model.payoutEther", _modelID)), _payout);
    }
    database.setBool(keccak256(abi.encodePacked("model.payoutToken", _modelID, _tokenAddress)), _payout);
    return true;
  }

The Operator can choose to accept Ether and an unlimited number of ERC20 tokens if they want.

Creating Assets

To create assets you will use CrowdsaleGeneratorETH or CrowdsaleGeneratorERC20

For ERC-20 based crowdsales you would call createAssetOrderERC20() from the AssetManager account, effectively creating a new crowdsale.

  function createAssetOrderERC20(string _assetURI, string _ipfs, bytes32 _modelID, uint _fundingLength, uint _amountToRaise, uint _assetManagerPerc, uint _escrow, address _fundingToken, address _paymentToken)
  payable
  external
  {
    if(_paymentToken == address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE)){
      require(msg.value == _escrow);
    } else {
      require(msg.value == 0);
    }
    require(_amountToRaise >= 100, "Crowdsale goal is too small");
    require((_assetManagerPerc + database.uintStorage(keccak256(abi.encodePacked("platform.percentage")))) < 100, "Manager percent need to be less than 100");
    require(database.addressStorage(keccak256(abi.encodePacked("model.operator", _modelID))) != address(0), "Model not set");
    require(!database.boolStorage(keccak256(abi.encodePacked("asset.uri", _assetURI))), "Asset URI is not unique"); //Check that asset URI is unique
    address assetAddress = minter.cloneToken(_assetURI, _fundingToken);
    require(setCrowdsaleValues(assetAddress, _fundingLength, _amountToRaise));
    require(setAssetValues(assetAddress, _assetURI, _ipfs, _modelID, msg.sender, _assetManagerPerc, _amountToRaise, _fundingToken));
    uint minEscrow = calculateEscrowERC20(_amountToRaise, msg.sender, _modelID, _fundingToken);
    require(lockEscrowERC20(msg.sender, assetAddress, _paymentToken, _fundingToken, _escrow, minEscrow));
    events.asset('Asset funding started', _assetURI, assetAddress, msg.sender);
    events.asset('New asset ipfs', _ipfs, assetAddress, msg.sender);
  }

Funding Assets

To fund an asset you can use either CrowdsaleETH or CrowdsaleERC20

For ERC-20 based crowdsales you would call buyAssetOrderERC20() from the investor account:

  function buyAssetOrderERC20(address _assetAddress, uint _amount, address _paymentToken)
  external
  payable
  returns (bool) {
    require(database.addressStorage(keccak256(abi.encodePacked("asset.manager", _assetAddress))) != address(0), "Invalid asset");
    require(now <= database.uintStorage(keccak256(abi.encodePacked("crowdsale.deadline", _assetAddress))), "Past deadline");
    require(!database.boolStorage(keccak256(abi.encodePacked("crowdsale.finalized", _assetAddress))), "Crowdsale finalized");

    if(_paymentToken == address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE)){
      require(msg.value == _amount, 'Msg.value does not match amount');
    } else {
      require(msg.value == 0, 'Msg.value should equal zero');
    }
    ERC20 fundingToken = ERC20(DividendInterface(_assetAddress).getERC20());
    uint fundingRemaining = database.uintStorage(keccak256(abi.encodePacked("crowdsale.remaining", _assetAddress)));
    uint collected; //This will be the value received by the contract after any conversions
    uint amount; //The number of tokens that will be minted
    //Check if the payment token is the same as the funding token. If not, convert, else just collect the funds
    if(_paymentToken == address(fundingToken)){
      collected = collectPayment(msg.sender, _amount, fundingRemaining, fundingToken);
    } else {
      collected = convertTokens(msg.sender, _amount, fundingToken, ERC20(_paymentToken), fundingRemaining);
    }
    require(collected > 0);
    if(collected < fundingRemaining){
      amount = collected.mul(100).div(uint(100).add(database.uintStorage(keccak256(abi.encodePacked("platform.fee")))));
      database.setUint(keccak256(abi.encodePacked("crowdsale.remaining", _assetAddress)), fundingRemaining.sub(collected));
      require(minter.mintAssetTokens(_assetAddress, msg.sender, amount), "Investor minting failed");
      require(fundingToken.transfer(address(reserve), collected));
    } else {
      amount = fundingRemaining.mul(100).div(uint(100).add(database.uintStorage(keccak256(abi.encodePacked("platform.fee")))));
      database.setBool(keccak256(abi.encodePacked("crowdsale.finalized", _assetAddress)), true);
      database.deleteUint(keccak256(abi.encodePacked("crowdsale.remaining", _assetAddress)));
      require(minter.mintAssetTokens(_assetAddress, msg.sender, amount), "Investor minting failed");   // Send remaining asset tokens to investor
      require(fundingToken.transfer(address(reserve), fundingRemaining));
      if(collected > fundingRemaining){
        require(fundingToken.transfer(msg.sender, collected.sub(fundingRemaining)));    // return extra funds
      }
    }
    events.transaction('Asset purchased', address(this), msg.sender, amount, _assetAddress);
    return true;
  }

After a crowdsale is funded, anyone can initiate payout to the asset manager:

  function payoutERC20(address _assetAddress)
  external
  whenNotPaused
  returns (bool) {
    require(database.boolStorage(keccak256(abi.encodePacked("crowdsale.finalized", _assetAddress))), "Crowdsale not finalized");
    require(!database.boolStorage(keccak256(abi.encodePacked("crowdsale.paid", _assetAddress))), "Crowdsale has paid out");
    //Set paid to true
    database.setBool(keccak256(abi.encodePacked("crowdsale.paid", _assetAddress)), true);
    //Setup token
    address fundingToken = DividendInterface(_assetAddress).getERC20();
    //Mint tokens for the asset manager and platform
    address platformAssetsWallet = database.addressStorage(keccak256(abi.encodePacked("platform.wallet.assets")));
    require(platformAssetsWallet != address(0), "Platform assets wallet not set");
    require(minter.mintAssetTokens(_assetAddress, database.addressStorage(keccak256(abi.encodePacked("contract", "AssetManagerFunds"))), database.uintStorage(keccak256(abi.encodePacked("asset.managerTokens", _assetAddress)))), "Manager minting failed");
    require(minter.mintAssetTokens(_assetAddress, platformAssetsWallet, database.uintStorage(keccak256(abi.encodePacked("asset.platformTokens", _assetAddress)))), "Platform minting failed");
    //Get the addresses for the receiver and platform
    address receiver = database.addressStorage(keccak256(abi.encodePacked("asset.manager", _assetAddress)));
    address platformFundsWallet = database.addressStorage(keccak256(abi.encodePacked("platform.wallet.funds")));
    require(receiver != address(0) && platformFundsWallet != address(0), "Platform funds walllet or receiver address not set");
    //Calculate amounts for platform and receiver
    uint amount = database.uintStorage(keccak256(abi.encodePacked("crowdsale.goal", _assetAddress)));
    uint platformFee = amount.getFractionalAmount(database.uintStorage(keccak256(abi.encodePacked("platform.fee"))));
    //Transfer funds to receiver and platform
    require(reserve.issueERC20(platformFundsWallet, platformFee, fundingToken), 'Platform funds not paid');
    require(reserve.issueERC20(receiver, amount, fundingToken), 'Receiver funds not paid');
    //Delete crowdsale start time
    database.deleteUint(keccak256(abi.encodePacked("crowdsale.start", _assetAddress)));
    //Increase asset count for manager
    address manager = database.addressStorage(keccak256(abi.encodePacked("asset.manager", _assetAddress)));
    database.setUint(keccak256(abi.encodePacked("manager.assets", manager)), database.uintStorage(keccak256(abi.encodePacked("manager.assets", manager))).add(1));
    //Emit event
    events.transaction('Asset payout', _assetAddress, receiver, amount, fundingToken);
    return true;
  }

If the funding fails you can call refund, which sends all funds to the asset-token contract to be redistributed to investors.

  function refund(address _assetAddress)
  public
  whenNotPaused
  validAsset(_assetAddress)
  afterDeadline(_assetAddress)
  notFinalized(_assetAddress)
  returns (bool) {
    require(database.uintStorage(keccak256(abi.encodePacked("crowdsale.deadline", _assetAddress))) != 0);
    database.deleteUint(keccak256(abi.encodePacked("crowdsale.deadline", _assetAddress)));
    DividendInterface assetToken = DividendInterface(_assetAddress);
    address tokenAddress = assetToken.getERC20();
    uint refundValue = assetToken.totalSupply().mul(uint(100).add(database.uintStorage(keccak256(abi.encodePacked("platform.fee"))))).div(100); //total supply plus platform fees
    reserve.refundERC20Asset(_assetAddress, refundValue, tokenAddress);
    return true;
  }

An asset manager may cancel the crowdsale at anytime prior to the crowdsale finshing to initiate a refund.

  function cancel(address _assetAddress)
  external
  whenNotPaused
  validAsset(_assetAddress)
  beforeDeadline(_assetAddress)
  notFinalized(_assetAddress)
  returns (bool){
    require(msg.sender == database.addressStorage(keccak256(abi.encodePacked("asset.manager", _assetAddress))));
    database.setUint(keccak256(abi.encodePacked("crowdsale.deadline", _assetAddress)), 1);
    refund(_assetAddress);
  }

After a crowdsale refund, investors may retrieve their funds by calling the withdraw function of their asset token contract.

Distributing Revenue

By default all assets generated on the platform are able to receive payments and distribute revenue according to tokens held by investors. It accomplishes this by keeping track of how much value (WEI/Token) is contained in each asset token. The token contract can receive payment in it's fallback function or by calling issueDividends()

Investors can withdraw income by calling withdraw() which updates their personal ledger:

  function withdraw()
  external
  updateIncomeClaimed(msg.sender)
  returns (bool) {
      uint amount = incomeOwed[msg.sender].div(scalingFactor);
      delete incomeOwed[msg.sender];
      assetIncomeIssued = assetIncomeIssued.add(amount);
      if(address(erc20) == address(0)){
        msg.sender.transfer(amount);
      } else {
        require(erc20.transfer(msg.sender, amount));
      }
      emit LogIncomeCollected(msg.sender, amount);
      return true;
  }

Onboarding Assets

Using the AssetGenerator contract, users are able to create already funded assets and manage them using the SDK's.

To create a funded asset call:

  function createAsset(string _tokenURI, address[] _tokenHolders, uint[] _amount)
  external
  returns (bool) {
    require (_tokenHolders.length == _amount.length && _tokenHolders.length <= 100);
    FixedDistribution assetInstance = new FixedDistribution(_tokenURI, _tokenHolders, _amount);
    database.setAddress(keccak256(abi.encodePacked("asset.manager", address(assetInstance))), msg.sender);
    events.asset('Asset created', _tokenURI, address(assetInstance), msg.sender);
    return true;
  }

If you want the asset to be tradeable on ERC20 exchanges or capable of being turned into a DAO, just call:

  function createTradeableAsset(string _tokenURI, address[] _tokenHolders, uint[] _amount)
  external
  returns (bool) {
    require (_tokenHolders.length == _amount.length && _tokenHolders.length <= uint8(100));
    address assetAddress = minter.cloneToken(_tokenURI, address(0));
    for (uint8 i = 0; i < _tokenHolders.length; i++) {
      minter.mintAssetTokens(assetAddress, _tokenHolders[i], _amount[i]);
    }
    database.setAddress(keccak256(abi.encodePacked("asset.manager", assetAddress)), msg.sender);
    events.asset('Asset created', _tokenURI, assetAddress, msg.sender);
    return true;
  }

Watching for crowdsale events

To watch for crowdsale events you must instantiate the Events contract and watch for the LogAsset event and LogTransaction event. Events related to the creation of an asset will be under the LogAsset event and events related to purchasing shares in an asset or when crowdsales payout to the asset manager will be under LogTransaction.

Here is how you will watch for asset creation events:

  eventsContract.events.LogAsset(
    {
      filter: {
        messageID: web3.utils.sha3('Asset funding started'),
        assetID: web3.utils.sha3('Asset URI'),
        origin: '0x0000...'
      },
      fromBlock: 0
    }, (error, event) => {
      if(error) console.log(error);
      if(event) {
        //Do something...
        console.log(event);
      }
    });

All filter values are optional. The message ID is just a bytes32 hash of the event message. The asset ID is a bytes32 hash of the asset name (or a bytes32 hash of an IPFS hash depending on the event). The origin is the address that initiated the transaction.

The event message is used to differentiate different events that are emitted over the same function. There are two relevant events under LogAsset. The first is 'Asset funding started' which logs when an asset is created. The second is 'New asset ipfs' which is also emitted on asset creation but also when an asset's IPFS hash is updated.

Watching funding:

  eventsContract.events.LogTransaction(
    {
      filter: {
        messageID: web3.utils.sha3('Asset purchased'),
        from: '0x0000...',
        to: '0x0000...'
      },
      fromBlock: 0
    }, (error, event) => {
      if(error) console.log(error);
      if(event) {
        //Do something...
        console.log(event);
      }
    });

Just like LogAsset, we can filter using the messageID parameter. In this case 'Asset purchased' is used to track when an investor has contributed towards a crowdsale. If you'd like to watch for when a crowdsale pays out, you can use the message 'Asset payout'. Additionally, you can filter for who sends the funds by using the from parameter. Or who receives the funds using the to parameter. In the case of funding the crowdsale, the to parameter would be the crowdsale contract. While on asset payout, the from parameter would be the contract.

Governance

Our goal is to enable this platform to be managed completely autonomously via a DAO. We have developed several Aragon apps to achieve this goal.

MyBit Go: Platform

Any changes to the platform settings can be changed by the MyBit DAO using this app. This includes changing platform wallets, fees, collateral requirements, and updating contracts.

MyBit Go: Operators

Potential operators must submit an application to the MyBit DAO. After a successful vote, they will be registered on the platform.

MyAsset

After a crowdsale has been funded, anyone can spin up a DAO to manage the asset. This will allow investors to vote to change the asset manager, burn their stake, requests more funding for the asset, and contribute additional funds.

✏️ All contracts are written in Solidity version 0.4.24.

Documentation

Documentation is created using Solidity-Docgen

cd docs/website
yarn build

To publish to GitHub Pages

cd docs/website
GIT_USER=<GIT_USER> \
  USE_SSH=true \
  yarn run publish-gh-pages

⚠️ This application is unstable and has not undergone any rigorous security audits. Use at your own risk.

MyBit Platform™ CHE-177.186.963

Readme

Keywords

none

Package Sidebar

Install

npm i @mybit/contracts

Weekly Downloads

0

Version

0.1.38

License

ISC

Unpacked Size

1.01 MB

Total Files

127

Last publish

Collaborators

  • csmartins
  • jjperezaguinaga
  • mybit-bot
  • pbteja
  • pmphillips