├── .gitignore ├── migrations ├── 1_initial_migration.js └── 2_test_migrations.js ├── README.md ├── test ├── contracts │ ├── index.js │ └── simplePOS.js └── utils │ └── testUtils.js ├── contracts ├── Migrations.sol ├── interfaces │ └── IUniswapExchange.sol ├── test-helpers │ ├── MockStableCoin.sol │ └── MockUniswapExchange.sol ├── SimplePOSToken.sol └── SimplePOS.sol ├── package.json ├── LICENSE └── truffle-config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | .secret 4 | .infura 5 | -------------------------------------------------------------------------------- /migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | const Migrations = artifacts.require("Migrations"); 2 | 3 | module.exports = function(deployer) { 4 | deployer.deploy(Migrations); 5 | }; 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple Point of Sale 2 | ``` 3 | npm install 4 | npm test 5 | ``` 6 | 7 | # Acknowledgment 8 | Thanks to [Synthetix](https://github.com/Synthetixio/synthetix) team for inspiration and code examples for test utils. -------------------------------------------------------------------------------- /test/contracts/index.js: -------------------------------------------------------------------------------- 1 | const { assertRevert } = require('../utils/testUtils') 2 | 3 | // So we don't have to constantly import our assert helpers everywhere 4 | // we'll just tag them onto the assert object for easy access. 5 | assert.revert = assertRevert 6 | -------------------------------------------------------------------------------- /migrations/2_test_migrations.js: -------------------------------------------------------------------------------- 1 | const MockUniswapExchange = artifacts.require("MockUniswapExchange") 2 | const SimplePOS = artifacts.require("SimplePOS") 3 | 4 | module.exports = function(deployer) { 5 | deployer.deploy(MockUniswapExchange).then(function() { 6 | return deployer.deploy(SimplePOS, MockUniswapExchange.address, "MyToken", "simMTKN", 1, 500, 5000, { value: 1000000000000000000 }); 7 | }) 8 | }; 9 | -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.4.21 <0.7.0; 2 | 3 | contract Migrations { 4 | address public owner; 5 | uint public last_completed_migration; 6 | 7 | constructor() public { 8 | owner = msg.sender; 9 | } 10 | 11 | modifier restricted() { 12 | if (msg.sender == owner) _; 13 | } 14 | 15 | function setCompleted(uint completed) public restricted { 16 | last_completed_migration = completed; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /contracts/interfaces/IUniswapExchange.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.6.0 <0.7.0; 2 | 3 | abstract contract IUniswapExchange { 4 | // Address of ERC20 token sold on this exchange 5 | function tokenAddress() external view virtual returns (address token); 6 | // Trade ETH to ERC20 7 | function ethToTokenSwapInput(uint256 min_tokens, uint256 deadline) external payable virtual returns (uint256 tokens_bought); 8 | // Trade ERC20 to ERC20 9 | function tokenToTokenSwapInput(uint256 tokens_sold, uint256 min_tokens_bought, uint256 min_eth_bought, uint256 deadline, address token_addr) external virtual returns (uint256 tokens_bought); 10 | } 11 | -------------------------------------------------------------------------------- /contracts/test-helpers/MockStableCoin.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.6.0 <0.7.0; 2 | 3 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 4 | import "@openzeppelin/contracts/access/AccessControl.sol"; 5 | 6 | contract MockStableCoin is ERC20, AccessControl { 7 | bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); 8 | 9 | constructor (string memory name, string memory symbol) ERC20(name, symbol) public { 10 | _setupRole(MINTER_ROLE, msg.sender); 11 | } 12 | 13 | function mint(address to, uint256 amount) public { 14 | require(hasRole(MINTER_ROLE, msg.sender), "Caller is not a minter"); 15 | _mint(to, amount); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /contracts/SimplePOSToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.6.0 <0.7.0; 2 | 3 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 4 | import "@openzeppelin/contracts/access/AccessControl.sol"; 5 | 6 | contract SimplePOSToken is ERC20, AccessControl { 7 | bytes32 public constant TOKEN_MANAGER_ROLE = keccak256("TOKEN_MANAGER_ROLE"); 8 | 9 | constructor (string memory name, string memory symbol) ERC20(name, symbol) public { 10 | _setupRole(TOKEN_MANAGER_ROLE, msg.sender); 11 | } 12 | 13 | function mint(address to, uint256 amount) public { 14 | require(hasRole(TOKEN_MANAGER_ROLE, msg.sender), "Caller is not a token manager"); 15 | _mint(to, amount); 16 | } 17 | 18 | function burn(address from, uint256 amount) public { 19 | require(hasRole(TOKEN_MANAGER_ROLE, msg.sender), "Caller is not a token manager"); 20 | _burn(from, amount); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-pos", 3 | "version": "1.0.0", 4 | "description": "Simple Poin of Sale smart contract", 5 | "main": "README.md", 6 | "scripts": { 7 | "test": "concurrently --kill-others --success first \"npm run ganache > /dev/null\" \"wait-port 8545 && truffle compile && truffle test\"", 8 | "ganache": "ganache-cli -e 10000" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/sche/simple-pos.git" 13 | }, 14 | "keywords": [ 15 | "smart contract", 16 | "point of sale" 17 | ], 18 | "author": "Andrey Scherbovich", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/sche/simple-pos/issues" 22 | }, 23 | "homepage": "https://github.com/sche/simple-pos#readme", 24 | "devDependencies": { 25 | "@openzeppelin/contracts": "^3.1.0", 26 | "@truffle/hdwallet-provider": "^1.0.42", 27 | "concurrently": "^5.1.0", 28 | "ganache-cli": "^6.9.0", 29 | "truffle-assertions": "^0.9.2", 30 | "wait-port": "^0.2.7" 31 | }, 32 | "dependencies": {} 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Andrey Scherbovich 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 | -------------------------------------------------------------------------------- /test/utils/testUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Convenience method to assert that the return of the given block when invoked or promise causes a 3 | * revert to occur, with an optional revert message. 4 | * @param blockOrPromise The JS block (i.e. function that when invoked returns a promise) or a promise itself 5 | * @param reason Optional reason string to search for in revert message 6 | */ 7 | const assertRevert = async (blockOrPromise, reason) => { 8 | let errorCaught = false; 9 | try { 10 | const result = typeof blockOrPromise === 'function' ? blockOrPromise() : blockOrPromise; 11 | await result; 12 | } catch (error) { 13 | assert.include(error.message, 'revert'); 14 | if (reason) { 15 | assert.include(error.message, reason); 16 | } 17 | errorCaught = true; 18 | } 19 | 20 | assert.equal(errorCaught, true, 'Operation did not revert as expected'); 21 | }; 22 | 23 | /** 24 | * Translates an amount to Eth unit (10^18). 25 | * @param amount The amount you want to re-base to UNIT 26 | */ 27 | const toEth = amount => web3.utils.toBN(web3.utils.toWei(amount.toString(), 'ether')); 28 | const fromEth = amount => web3.utils.fromWei(amount, 'ether'); 29 | 30 | module.exports = { 31 | assertRevert, 32 | toEth, 33 | fromEth 34 | }; 35 | -------------------------------------------------------------------------------- /contracts/test-helpers/MockUniswapExchange.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.6.0 <0.7.0; 2 | 3 | import "../interfaces/IUniswapExchange.sol"; 4 | import "./MockStableCoin.sol"; 5 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 6 | 7 | contract MockUniswapExchange is IUniswapExchange { 8 | MockStableCoin _exchangeToken; 9 | uint _ethToTokenSwapRate; 10 | uint _tokenToTokenSwapRate; 11 | 12 | event MockExchangeCreated(address indexed stableCoin); 13 | 14 | constructor () 15 | public 16 | { 17 | _exchangeToken = new MockStableCoin("Stable Coin", "STBL"); 18 | _ethToTokenSwapRate = 1; 19 | _tokenToTokenSwapRate = 1; 20 | emit MockExchangeCreated(address(_exchangeToken)); 21 | } 22 | 23 | // Address of ERC20 token sold on this exchange 24 | function tokenAddress() 25 | external 26 | view 27 | override 28 | returns (address token) 29 | { 30 | return address(_exchangeToken); 31 | } 32 | 33 | // Trade ETH to ERC20 34 | function ethToTokenSwapInput(uint256 min_tokens, uint256 deadline) 35 | external 36 | payable 37 | override 38 | returns (uint256 tokens_bought) 39 | { 40 | tokens_bought = msg.value * _ethToTokenSwapRate; 41 | _exchangeToken.mint(msg.sender, tokens_bought); 42 | } 43 | 44 | // Trade ERC20 to ERC20 45 | function tokenToTokenSwapInput(uint256 tokens_sold, uint256 min_tokens_bought, uint256 min_eth_bought, uint256 deadline, address token_addr) 46 | external 47 | override 48 | returns (uint256 tokens_bought) 49 | { 50 | return tokens_sold * _tokenToTokenSwapRate; 51 | } 52 | 53 | function setEthToTokenSwapRate(uint newRate) 54 | external 55 | { 56 | _ethToTokenSwapRate = newRate; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /truffle-config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this file to configure your truffle project. It's seeded with some 3 | * common settings for different networks and features like migrations, 4 | * compilation and testing. Uncomment the ones you need or modify 5 | * them to suit your project as necessary. 6 | * 7 | * More information about configuration can be found at: 8 | * 9 | * truffleframework.com/docs/advanced/configuration 10 | * 11 | * To deploy via Infura you'll need a wallet provider (like truffle-hdwallet-provider) 12 | * to sign your transactions before they're sent to a remote public node. Infura accounts 13 | * are available for free at: infura.io/register. 14 | * 15 | * You'll also need a mnemonic - the twelve word phrase the wallet uses to generate 16 | * public/private key pairs. If you're publishing your code to GitHub make sure you load this 17 | * phrase from a file you've .gitignored so it doesn't accidentally become public. 18 | * 19 | */ 20 | 21 | const HDWalletProvider = require("@truffle/hdwallet-provider"); 22 | 23 | const fs = require('fs'); 24 | const mnemonic = fs.readFileSync(".secret").toString().trim(); 25 | const infuraKey = fs.readFileSync(".infura").toString().trim(); 26 | 27 | module.exports = { 28 | /** 29 | * Networks define how you connect to your ethereum client and let you set the 30 | * defaults web3 uses to send transactions. If you don't specify one truffle 31 | * will spin up a development blockchain for you on port 9545 when you 32 | * run `develop` or `test`. You can ask a truffle command to use a specific 33 | * network from the command line, e.g 34 | * 35 | * $ truffle test --network 36 | */ 37 | 38 | networks: { 39 | // Useful for testing. The `development` name is special - truffle uses it by default 40 | // if it's defined here and no other network is specified at the command line. 41 | // You should run a client (like ganache-cli, geth or parity) in a separate terminal 42 | // tab if you use this network and you must also set the `host`, `port` and `network_id` 43 | // options below to some value. 44 | 45 | development: { 46 | host: "localhost", // Localhost (default: none) 47 | port: 8545, // Standard Ethereum port (default: none) 48 | network_id: "*", // Any network (default: none) 49 | }, 50 | 51 | // Another network with more advanced options... 52 | // advanced: { 53 | // port: 8777, // Custom port 54 | // network_id: 1342, // Custom network 55 | // gas: 8500000, // Gas sent with each transaction (default: ~6700000) 56 | // gasPrice: 20000000000, // 20 gwei (in wei) (default: 100 gwei) 57 | // from:
, // Account to send txs from (default: accounts[0]) 58 | // websockets: true // Enable EventEmitter interface for web3 (default: false) 59 | // }, 60 | 61 | // Useful for deploying to a public network. 62 | // NB: It's important to wrap the provider as a function. 63 | rinkeby: { 64 | provider: () => new HDWalletProvider(mnemonic, `https://rinkeby.infura.io/v3/`.concat(infuraKey)), 65 | network_id: 4, // Rinkeby id 66 | gas: 5500000, 67 | confirmations: 2, // # of confs to wait between deployments. (default: 0) 68 | timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50) 69 | skipDryRun: true // Skip dry run before migrations? (default: false for public nets ) 70 | }, 71 | 72 | // Useful for private networks 73 | // private: { 74 | // provider: () => new HDWalletProvider(mnemonic, `https://network.io`), 75 | // network_id: 2111, // This network is yours, in the cloud. 76 | // production: true // Treats this network as if it was a public net. (default: false) 77 | // } 78 | }, 79 | 80 | // Set default mocha options here, use special reporters etc. 81 | mocha: { 82 | // timeout: 100000 83 | }, 84 | 85 | // Configure your compilers 86 | compilers: { 87 | solc: { 88 | version: "0.6.2", // Fetch exact version from solc-bin (default: truffle's version) 89 | // docker: true, // Use "0.5.1" you've installed locally with docker (default: false) 90 | settings: { // See the solidity docs for advice about optimization and evmVersion 91 | optimizer: { 92 | enabled: false, 93 | runs: 200 94 | }, 95 | // evmVersion: "byzantium" 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /contracts/SimplePOS.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.6.0 <0.7.0; 2 | 3 | import "./interfaces/IUniswapExchange.sol"; 4 | import "./SimplePOSToken.sol"; 5 | import "@openzeppelin/contracts/math/SafeMath.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 7 | 8 | contract SimplePOS { 9 | address payable public owner; 10 | IUniswapExchange public exchange; 11 | SimplePOSToken public sposToken; 12 | uint public commission; 13 | uint public curveCoefficient; 14 | 15 | using SafeMath for uint; 16 | 17 | /** 18 | * @dev Constructor 19 | * @param _exchange Exchange for bonus token owners 20 | * @param _sposTokenName SimplePOSToken name; SimplePOSToken controls bonus tokens pool 21 | * @param _sposTokenSymbol SimplePOSToken symbol; SimplePOSToken controls bonus tokens pool 22 | * @param _initialRatio SimplePOS creator sets up the initial ratio of bonus tokens pool to the SimplePOSToken supply 23 | * @param _commission Commission on incoming payments that should form bonus tokens pool; [0..10000); 1 unit == 0.01% 24 | * @param _curveCoefficient The bonding curve coefficient; [0..10000); 1 unit == 0.01% 25 | */ 26 | constructor( 27 | IUniswapExchange _exchange, 28 | string memory _sposTokenName, 29 | string memory _sposTokenSymbol, 30 | uint _initialRatio, 31 | uint _commission, 32 | uint _curveCoefficient) 33 | public 34 | payable 35 | { 36 | require(msg.value > 0, "ETH is required to form bonus tokens pool"); 37 | require(_initialRatio > 0, "Initial ratio should be positive"); 38 | require(_commission < 10000, "Commission should be less than 100%"); 39 | require(_curveCoefficient < 10000, "Curve coefficient should be less than 100%"); 40 | owner = msg.sender; 41 | exchange = _exchange; 42 | sposToken = new SimplePOSToken(_sposTokenName, _sposTokenSymbol); 43 | commission = _commission; 44 | curveCoefficient = _curveCoefficient; 45 | uint initialExchangeTokenValue = exchange.ethToTokenSwapInput.value(msg.value)(0, now); 46 | uint initialMintedPOSTokens = initialExchangeTokenValue.mul(_initialRatio); 47 | sposToken.mint(msg.sender, initialMintedPOSTokens); 48 | } 49 | 50 | receive() 51 | external 52 | payable 53 | { 54 | uint bonusPart = msg.value.mul(commission).div(10000); 55 | owner.transfer(msg.value.sub(bonusPart)); 56 | 57 | // Process incoming bonus tokens 58 | uint bonusTokenBalance = IERC20(exchange.tokenAddress()).balanceOf(address(this)); 59 | uint sposTokenSupply = sposToken.totalSupply(); 60 | // We calculate with a precesion of 4 numbers 61 | uint invariant = bonusTokenBalance.mul(10000).div(sposTokenSupply); 62 | uint incomingBonusTokens = exchange.ethToTokenSwapInput.value(bonusPart)(0, now); 63 | uint newBonusTokenBalanceForInvariant = bonusTokenBalance + incomingBonusTokens - incomingBonusTokens.mul(curveCoefficient).div(10000); 64 | uint toMintSPOSTokens = newBonusTokenBalanceForInvariant.mul(10000).div(invariant) - sposTokenSupply; 65 | sposToken.mint(msg.sender, toMintSPOSTokens); 66 | } 67 | 68 | /** 69 | * @dev Exchange SPOS tokens on bonus tokens. This action burns 'amount' of SPOS and transfers bonus tokens to a caller. 70 | * @param _amount Amount of SPOS tokens to exchange 71 | */ 72 | function exchangeSposTokensOnBonusTokens( 73 | uint _amount) 74 | public 75 | { 76 | // There should be always spos tokens minted to properly calculate invariant 77 | require(_amount < sposToken.totalSupply(), "SPOS token amount should be less than total supply."); 78 | // We do not check tokens availability here as 'burn' will check it 79 | IERC20 bonusToken = IERC20(exchange.tokenAddress()); 80 | // toTransferBonusTokens < bonusToken.balanceOf(address(this) will always be true 81 | uint toTransferBonusTokens = _amount.mul(bonusToken.balanceOf(address(this))).div(sposToken.totalSupply()); 82 | sposToken.burn(msg.sender, _amount); 83 | bonusToken.transfer(msg.sender, toTransferBonusTokens); 84 | } 85 | 86 | function getExchangeAddress() 87 | public 88 | view 89 | returns (address) 90 | { 91 | return address(exchange); 92 | } 93 | 94 | function getBonusTokenAddress() 95 | public 96 | view 97 | returns (address) 98 | { 99 | return exchange.tokenAddress(); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /test/contracts/simplePOS.js: -------------------------------------------------------------------------------- 1 | const SimplePOS = artifacts.require("SimplePOS") 2 | const SimplePOSToken = artifacts.require("SimplePOSToken") 3 | const MockUniswapExchange = artifacts.require("MockUniswapExchange") 4 | const MockStableCoin = artifacts.require("MockStableCoin") 5 | 6 | const { toEth, fromEth } = require('../utils/testUtils') 7 | const maxUint = "115792089237316195423570985008687907853269984665640564039457584007913129639935" // 2^256 - 1 8 | 9 | contract("SimplePOS", accounts => { 10 | /*************************************** 11 | ************* CONSTRUCTUR ************* 12 | ***************************************/ 13 | 14 | it("should create a contract", async () => { 15 | let exchange = await MockUniswapExchange.new() 16 | let initialEthValue = toEth(0.1) 17 | let contract = await SimplePOS.new(exchange.address, "MyToken", "simMTKN", 1, 100, 5000, { value: initialEthValue }) 18 | // owner should be the creator 19 | assert.equal(await contract.owner(), accounts[0]) 20 | // check SPOS token params 21 | let sposToken = await SimplePOSToken.at(await contract.sposToken()) 22 | assert.equal(await sposToken.name(), "MyToken") 23 | assert.equal(await sposToken.symbol(), "simMTKN") 24 | // check commission 25 | assert.equal(await contract.commission(), 100) 26 | // check curve coefficient 27 | assert.equal(await contract.curveCoefficient(), 5000) 28 | // check exchange 29 | assert.equal(await contract.getExchangeAddress(), exchange.address) 30 | // check that bonus token is the same as exchange token 31 | assert.equal(await contract.getBonusTokenAddress(), await exchange.tokenAddress()) 32 | // check that SPOS token is minted in the right proportion (MockUniswapExchange._ethToTokenSwapRate == 1) 33 | // and transfered to the creator 34 | let totalSupply = await sposToken.totalSupply() 35 | assert.equal(fromEth(totalSupply), fromEth(initialEthValue)) 36 | // check that the contract creator gets minted SPOS tokens 37 | let creatorBalance = await sposToken.balanceOf(accounts[0]) 38 | assert.equal(fromEth(creatorBalance), fromEth(initialEthValue)) 39 | // check that SimplePOS contract controls bonus pool 40 | // (1 to 1 with sposToken.totalSupply as contract.initialRation == 1) 41 | let mockBonusToken = await MockStableCoin.at(await exchange.tokenAddress()) 42 | assert.equal(fromEth(await mockBonusToken.balanceOf(contract.address)), fromEth(totalSupply)) 43 | // check that contract Eth balance is zero 44 | assert.equal(await web3.eth.getBalance(contract.address), 0) 45 | }) 46 | 47 | it("should revert constructor if commission is 100%", async () => { 48 | let exchange = await MockUniswapExchange.new() 49 | await assert.revert(SimplePOS.new(exchange.address, "MyToken", "simMTKN", 1, 10000, 5000, { value: toEth(0.1) }), 50 | "Commission should be less than 100%") 51 | }) 52 | 53 | it("should revert constructor if value is not transfered", async () => { 54 | let exchange = await MockUniswapExchange.new() 55 | await assert.revert(SimplePOS.new(exchange.address, "MyToken", "simMTKN", 1, 100, 5000), 56 | "ETH is required to form bonus tokens pool") 57 | }) 58 | 59 | it("should revert constructor if initial ratio is zero", async () => { 60 | let exchange = await MockUniswapExchange.new() 61 | await assert.revert(SimplePOS.new(exchange.address, "MyToken", "simMTKN", 0, 100, 5000, { value: toEth(0.1) }), 62 | "Initial ratio should be positive") 63 | }) 64 | 65 | it("should revert constructor if curve coefficient is 100%", async () => { 66 | let exchange = await MockUniswapExchange.new() 67 | await assert.revert(SimplePOS.new(exchange.address, "MyToken", "simMTKN", 1, 100, 10000, { value: toEth(0.1) }), 68 | "Curve coefficient should be less than 100%") 69 | }) 70 | 71 | it("should revert constructor if initialMintedPOSTokens is overflowed", async () => { 72 | let exchange = await MockUniswapExchange.new() 73 | let oneWei = toEth(fromEth("1")) 74 | let contract = await SimplePOS.new(exchange.address, "MyToken", "simMTKN", maxUint, 100, 5000, { value: oneWei }) 75 | let sposToken = await SimplePOSToken.at(await contract.sposToken()) 76 | let totalSupply = await sposToken.totalSupply() 77 | assert.equal(fromEth(totalSupply), fromEth(maxUint)) 78 | // should overflow 79 | await exchange.setEthToTokenSwapRate(2) 80 | await assert.revert(SimplePOS.new(exchange.address, "MyToken", "simMTKN", maxUint, 100, 5000, { value: oneWei }), 81 | "SafeMath: multiplication overflow.") 82 | }) 83 | 84 | /*************************************** 85 | ************ RECEIVE ETHER ************ 86 | ***************************************/ 87 | 88 | it("should receive payements and mint bonus tokens accordingly", async () => { 89 | let exchange = await MockUniswapExchange.new() 90 | let mockBonusToken = await MockStableCoin.at(await exchange.tokenAddress()) 91 | 92 | let initialEthValue = toEth(1) 93 | // fee: 5%; curve_coefficient: 50% 94 | let contract = await SimplePOS.new(exchange.address, "MyToken", "simMTKN", 1, 500, 5000, { value: initialEthValue }) 95 | 96 | // check that SPOS token is minted in the right proportion (MockUniswapExchange._ethToTokenSwapRate == 1) 97 | // and transfered to the creator 98 | let sposToken = await SimplePOSToken.at(await contract.sposToken()) 99 | let totalSupply = await sposToken.totalSupply() 100 | assert.equal(fromEth(totalSupply), fromEth(initialEthValue)) // 1 101 | let creatorBalance = await sposToken.balanceOf(accounts[0]) 102 | assert.equal(fromEth(creatorBalance), fromEth(initialEthValue)) 103 | 104 | // Check values against test vectors 105 | let testVector = [ 106 | { 'account': accounts[1], 'eth': 1, 'idp': 1.05, 'spos': 1.025 }, 107 | { 'account': accounts[2], 'eth': 5, 'idp': 1.3, 'spos': 1.147124865761983793 }, 108 | { 'account': accounts[3], 'eth': 10, 'idp': 1.8, 'spos': 1.367807977409106953 }, 109 | { 'account': accounts[4], 'eth': 50, 'idp': 4.3, 'spos': 2.317805304354434227 } 110 | ] 111 | for (var i in testVector) { 112 | let v = testVector[i] 113 | await contract.sendTransaction({from: v.account, value: toEth(v.eth)}) 114 | assert.equal(fromEth(await mockBonusToken.balanceOf(contract.address)), v.idp) 115 | assert.equal(fromEth(await sposToken.totalSupply()), v.spos) 116 | } 117 | assert.equal(fromEth(await sposToken.balanceOf(accounts[4])), 0.949997326945327274) 118 | }) 119 | 120 | /*************************************** 121 | ********** EXCHANGE TOKENS ************ 122 | ***************************************/ 123 | 124 | it("allows exchanging SPOS tokens", async () => { 125 | let exchange = await MockUniswapExchange.new() 126 | let mockBonusToken = await MockStableCoin.at(await exchange.tokenAddress()) 127 | 128 | let initialEthValue = toEth(1) 129 | // fee: 5%; curve_coefficient: 50% 130 | let contract = await SimplePOS.new(exchange.address, "MyToken", "simMTKN", 1, 500, 5000, { value: initialEthValue }) 131 | let sposToken = await SimplePOSToken.at(await contract.sposToken()) 132 | 133 | await contract.sendTransaction({ from: accounts[1], value: toEth(1) }) 134 | assert.equal(fromEth(await sposToken.balanceOf(accounts[1])), 0.025) // accounts[1] SPOS token balance is '0.025' after this step 135 | 136 | // exchange the rest 0.015 out of 0.025 spos tokens from account1 137 | await contract.exchangeSposTokensOnBonusTokens(toEth(0.015), { from: accounts[1] }) 138 | assert.equal(fromEth(await mockBonusToken.balanceOf(accounts[1])), 0.015365853658536585) // 0.015 / 1.025 (spos tokens total) * 1.05 (bonus tokens total) = 0.0126 139 | 140 | assert.equal(fromEth(await sposToken.totalSupply()), 1.01) 141 | assert.equal(fromEth(await mockBonusToken.balanceOf(contract.address)), 1.034634146341463415) 142 | 143 | // exchange the rest 0.01 spos tokens from account1 144 | await contract.exchangeSposTokensOnBonusTokens(toEth(0.01), { from: accounts[1] }) 145 | assert.equal(fromEth(await mockBonusToken.balanceOf(accounts[1])), 0.025609756097560975) 146 | }) 147 | 148 | it("should revert exchange SPOS tokens if it equal to the total supply", async () => { 149 | let exchange = await MockUniswapExchange.new() 150 | 151 | let initialEthValue = toEth(1) 152 | // fee: 5%; curve_coefficient: 50% 153 | let contract = await SimplePOS.new(exchange.address, "MyToken", "simMTKN", 1, 500, 5000, { value: initialEthValue }) 154 | let sposToken = await SimplePOSToken.at(await contract.sposToken()) 155 | 156 | await contract.sendTransaction({from: accounts[1], value: toEth(1)}) 157 | assert.equal(fromEth(await sposToken.balanceOf(accounts[0])), 1) 158 | assert.equal(fromEth(await sposToken.balanceOf(accounts[1])), 0.025) 159 | 160 | contract.exchangeSposTokensOnBonusTokens(toEth(1), { from: accounts[0] }) 161 | 162 | await assert.revert(contract.exchangeSposTokensOnBonusTokens(toEth(0.025), {from: accounts[1]}), 163 | "SPOS token amount should be less than total supply.") 164 | }) 165 | 166 | it("should revert exchange SPOS tokens if it exceeding the sender balance", async () => { 167 | let exchange = await MockUniswapExchange.new() 168 | 169 | let initialEthValue = toEth(1) 170 | // fee: 5%; curve_coefficient: 50% 171 | let contract = await SimplePOS.new(exchange.address, "MyToken", "simMTKN", 1, 500, 5000, { value: initialEthValue }) 172 | let sposToken = await SimplePOSToken.at(await contract.sposToken()) 173 | 174 | await contract.sendTransaction({from: accounts[1], value: toEth(1)}) 175 | assert.equal(fromEth(await sposToken.balanceOf(accounts[1])), 0.025) // accounts[1] SPOS token balance is '0.025' after this step 176 | 177 | await assert.revert(contract.exchangeSposTokensOnBonusTokens(toEth(0.026), {from: accounts[1]}), 178 | "ERC20: burn amount exceeds balance.") 179 | }) 180 | 181 | }) 182 | --------------------------------------------------------------------------------