├── .gitignore ├── contracts ├── EtherPriceOracleInterface.sol ├── MockEtherPriceOracle.sol ├── Migrations.sol ├── ERC20Interface.sol ├── ERC20Token.sol └── EthUSD.sol ├── migrations ├── 1_initial_migration.js └── 2_deploy_contracts.js ├── package.json ├── truffle.js ├── README.md ├── test └── ethusd.js └── EthUSD.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | .env 4 | -------------------------------------------------------------------------------- /contracts/EtherPriceOracleInterface.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.15; 2 | 3 | contract EtherPriceOracleInterface { 4 | function price() public constant returns (uint); 5 | } 6 | -------------------------------------------------------------------------------- /migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | var Migrations = artifacts.require("./Migrations.sol"); 2 | 3 | module.exports = function(deployer) { 4 | deployer.deploy(Migrations); 5 | }; 6 | -------------------------------------------------------------------------------- /contracts/MockEtherPriceOracle.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.15; 2 | 3 | import "./EtherPriceOracleInterface.sol"; 4 | 5 | contract MockEtherPriceOracle is EtherPriceOracleInterface { 6 | uint public price_; 7 | 8 | function setPrice(uint price) public { 9 | price_ = price; 10 | } 11 | function price() public constant returns (uint){ 12 | return price_; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.4; 2 | 3 | contract Migrations { 4 | address public owner; 5 | uint public last_completed_migration; 6 | 7 | modifier restricted() { 8 | if (msg.sender == owner) _; 9 | } 10 | 11 | function Migrations() { 12 | owner = msg.sender; 13 | } 14 | 15 | function setCompleted(uint completed) restricted { 16 | last_completed_migration = completed; 17 | } 18 | 19 | function upgrade(address new_address) restricted { 20 | Migrations upgraded = Migrations(new_address); 21 | upgraded.setCompleted(last_completed_migration); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ethusd", 3 | "version": "0.0.1", 4 | "description": "", 5 | "main": "truffle.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "./node_modules/truffle/build/cli.bundled.js test" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/masonforest/ethusd.git" 15 | }, 16 | "author": "", 17 | "license": "ISC", 18 | "bugs": { 19 | "url": "https://github.com/masonforest/ethusd/issues" 20 | }, 21 | "homepage": "https://github.com/masonforest/ethusd#readme", 22 | "dependencies": { 23 | "solc": "^0.4.0", 24 | "truffle": "^3.4.11" 25 | }, 26 | "devDependencies": { 27 | "bluebird": "^3.5.1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /migrations/2_deploy_contracts.js: -------------------------------------------------------------------------------- 1 | var EthUSD = artifacts.require("./EthUSD.sol"); 2 | var MockEtherPriceOracle = artifacts.require("./MockEtherPriceOracle.sol"); 3 | const priceOraclesByNework = { 4 | mainnet: "0xf5c600cda3b7289b2863872b23084527fb4c6107", 5 | ropsten: "0x9f7f9f38825012623cae7982ead15cc0571adcde", 6 | rinkeby: "0xdcd37d397ab4eeb86ab8aed69fdc551f7c0bc77b", 7 | kovan: "0x9e803018893dc7ec24c19c8779a4444ba49c44e4", 8 | }; 9 | 10 | module.exports = function(deployer, network) { 11 | if (network == "development") { 12 | deployer.deploy(MockEtherPriceOracle).then(() => { 13 | return deployer.deploy(EthUSD, MockEtherPriceOracle.address); 14 | }); 15 | } else { 16 | deployer.deploy(EthUSD, priceOraclesByNework[network]); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /truffle.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const _ = require("lodash"); 3 | const WalletProvider = require("truffle-wallet-provider"); 4 | const Wallet = require('ethereumjs-wallet'); 5 | 6 | const networks = ["rinkeby", "kovan", "ropsten", "mainnet"]; 7 | 8 | const infuraNetworks = _.fromPairs(_.compact(networks.map((network) => { 9 | var envVarName = `${network.toUpperCase()}_PRIVATE_KEY` 10 | var privateKeyHex = process.env[envVarName]; 11 | 12 | if(privateKeyHex) { 13 | var privateKey = new Buffer(process.env[envVarName], "hex") 14 | var wallet = Wallet.fromPrivateKey(privateKey); 15 | var provider = new WalletProvider(wallet, `https://${network}.infura.io/`); 16 | 17 | return [ 18 | network, 19 | { 20 | host: "localhost", 21 | port: 8545, 22 | network_id: "*", 23 | gas: 4612388, 24 | provider, 25 | } 26 | ]; 27 | } 28 | }))); 29 | 30 | module.exports = { 31 | networks: { 32 | development: { 33 | host: "localhost", 34 | port: 8545, 35 | network_id: "*" 36 | }, 37 | ...infuraNetworks, 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /contracts/ERC20Interface.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.15; 2 | 3 | contract ERC20Interface { 4 | // Get the total token supply 5 | function totalSupply() constant returns (uint256); 6 | 7 | // Get the account balance of another account with address _owner 8 | function balanceOf(address _owner) constant returns (uint256 balance); 9 | 10 | // Send _value amount of tokens to address _to 11 | function transfer(address _to, uint256 _value) returns (bool success); 12 | 13 | // Send _value amount of tokens from address _from to address _to 14 | function transferFrom(address _from, address _to, uint256 _value) returns (bool success); 15 | 16 | // Allow _spender to withdraw from your account, multiple times, up to the _value amount. 17 | // If this function is called again it overwrites the current allowance with _value. 18 | // this function is required for some DEX functionality 19 | function approve(address _spender, uint256 _value) returns (bool success); 20 | 21 | // Returns the amount which _spender is still allowed to withdraw from _owner 22 | function allowance(address _owner, address _spender) constant returns (uint256 remaining); 23 | 24 | // Triggered when tokens are transferred. 25 | event Transfer(address indexed _from, address indexed _to, uint256 _value); 26 | 27 | // Triggered whenever approve(address _spender, uint256 _value) is called. 28 | event Approval(address indexed _owner, address indexed _spender, uint256 _value); 29 | } 30 | -------------------------------------------------------------------------------- /contracts/ERC20Token.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.15; 2 | 3 | import "./ERC20Interface.sol"; 4 | 5 | contract ERC20Token is ERC20Interface { 6 | uint256 _totalSupply = 0; 7 | mapping(address => uint256) balances; 8 | mapping(address => mapping (address => uint256)) allowed; 9 | 10 | function totalSupply() constant returns (uint256) { 11 | return _totalSupply; 12 | } 13 | 14 | function balanceOf(address _owner) constant returns (uint256 balance) { 15 | return balances[_owner]; 16 | } 17 | 18 | function transfer(address _to, uint256 _amount) returns (bool success) { 19 | if (balances[msg.sender] >= _amount 20 | && _amount > 0 21 | && balances[_to] + _amount > balances[_to]) { 22 | balances[msg.sender] -= _amount; 23 | balances[_to] += _amount; 24 | Transfer(msg.sender, _to, _amount); 25 | return true; 26 | } else { 27 | return false; 28 | } 29 | } 30 | 31 | function transferFrom( 32 | address _from, 33 | address _to, 34 | uint256 _amount 35 | ) returns (bool success) { 36 | if (balances[_from] >= _amount 37 | && allowed[_from][msg.sender] >= _amount 38 | && _amount > 0 39 | && balances[_to] + _amount > balances[_to]) { 40 | balances[_from] -= _amount; 41 | allowed[_from][msg.sender] -= _amount; 42 | balances[_to] += _amount; 43 | Transfer(_from, _to, _amount); 44 | return true; 45 | } else { 46 | return false; 47 | } 48 | } 49 | 50 | function approve(address _spender, uint256 _amount) returns (bool success) { 51 | allowed[msg.sender][_spender] = _amount; 52 | Approval(msg.sender, _spender, _amount); 53 | return true; 54 | } 55 | 56 | function allowance(address _owner, address _spender) constant returns (uint256 remaining) { 57 | return allowed[_owner][_spender]; 58 | } 59 | } 60 | 61 | -------------------------------------------------------------------------------- /contracts/EthUSD.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.15; 2 | 3 | import "./ERC20Token.sol"; 4 | import "./EtherPriceOracleInterface.sol"; 5 | 6 | contract EthUSD is ERC20Token { 7 | EtherPriceOracleInterface priceOracle; 8 | string public constant symbol = "EthUSD"; 9 | string public constant name = "USD pegged Ether backed stablecoin"; 10 | uint8 public constant decimals = 2; 11 | 12 | function EthUSD(address etherPriceOracleAddress) { 13 | priceOracle = EtherPriceOracleInterface(etherPriceOracleAddress); 14 | } 15 | 16 | function donate() payable {} 17 | 18 | function issue() payable { 19 | uint amountInCents = (msg.value * priceOracle.price()) / 1 ether; 20 | _totalSupply += amountInCents; 21 | balances[msg.sender] += amountInCents; 22 | } 23 | 24 | function getPrice() returns (uint) { 25 | return priceOracle.price(); 26 | } 27 | 28 | function withdraw(uint amountInCents) returns (uint amountInWei){ 29 | assert(amountInCents <= balanceOf(msg.sender)); 30 | amountInWei = (amountInCents * 1 ether) / priceOracle.price(); 31 | 32 | // If we don't have enough Ether in the contract to pay out the full amount 33 | // pay an amount proportinal to what we have left. 34 | // this way user's net worth will never drop at a rate quicker than 35 | // the collateral itself. 36 | 37 | // For Example: 38 | // A user deposits 1 Ether when the price of Ether is $300 39 | // the price then falls to $150. 40 | // If we have enough Ether in the contract we cover ther losses 41 | // and pay them back 2 ether (the same amount in USD). 42 | // if we don't have enough money to pay them back we pay out 43 | // proportonailly to what we have left. In this case they'd 44 | // get back their original deposit of 1 Ether. 45 | if(this.balance <= amountInWei) { 46 | amountInWei = (amountInWei * this.balance * priceOracle.price()) / (1 ether * _totalSupply); 47 | } 48 | 49 | balances[msg.sender] -= amountInCents; 50 | _totalSupply -= amountInCents; 51 | msg.sender.transfer(amountInWei); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | __Disclaimer__ 2 | 3 | _This is a Proof of Concept only. Please don't deposit more than you're willing 4 | to loose._ 5 | 6 | EthUSD 7 | ==== 8 | 9 | EthUSD is an Ether backed stablecoin pegged to the USD. 10 | 11 | Users can issue EthUSD by paying Ether into the contract. At a later date they 12 | can withdraw Ether. If the price of Ether doubles and they withdraw they'll 13 | receive half as much Ether as they deposited (The same amount in USD). The remaining ether is stored in the contract to cover losses when the price 14 | falls. 15 | 16 | When a user has deposited funds and the price falls two different things can happen. If the contract has the funds to pay the user it will do so (Eg. If you bought in 2 Ether and the price halved you'd get back 4 Ethers). If the contract doesn't have enough to fully cover the losses it will pay out proportionally to what it has left (eg If you bought in 2 Ethers and the price halved but the contract only had 2 Ethers it'd give you back 2 Ethers). 17 | 18 | EthUSD is simpler than other stablecoins because it doesn't try to solve the bad collateral problem. 19 | If the price of the collateral (Ether) starts to fall the stablecoin will fall in value at at the same rate of depreciation. 20 | 21 | 22 | EtherUSD is deployed at the following addresses: 23 | 24 | | Network | Address | 25 | ------------|--------------- 26 | |Main Network | [0x914558f943d117660cc576f567240fbb433698c8](https://etherscan.io/address/0x914558f943d117660cc576f567240fbb433698c8) | 27 | Ropsten | [0x7a406e27b9f4feb58bf23e4fe36497a74882e2f8](https://ropsten.etherscan.io/address/0x7a406e27b9f4feb58bf23e4fe36497a74882e2f8) | 28 | Rinkeby | [0x39c688b428c51fdccace92517bec295440e55343](https://rinkeby.etherscan.io/address/0x39c688b428c51fdccace92517bec295440e55343) | 29 | Kovan | [0x33fb4e08ecd73e6d851c4234d87680eb7b641868](https://kovan.etherscan.io/address/0x33fb4e08ecd73e6d851c4234d87680eb7b641868) | 30 | 31 | Usage 32 | ----- 33 | 34 | To issue tokens yourself: 35 | 36 | 1. Install [MetaMask](https://metamask.io/) or another Ethereum enabled browser. 37 | 38 | 2. Visit https://wallet.ethereum.org/ 39 | 3. Click on "Contracts" 40 | 4. Click on "Watch Token" 41 | 5. Enter the contract address found in the table below. 42 | 6. Click "Watch Contract" 43 | 7. Enter the contract address found in the table above. 44 | 8. Copy and paste the [ABI](https://raw.githubusercontent.com/masonforest/ethusd/mf-convert-to-truffle/EthUSD.json). 45 | 9. Click on "EthUSD" 46 | 10. Under "Select function" choose "issue" 47 | 11. Enter the amount of EthUSD you'd like to issue 48 | 12. Click on "Execute" 49 | 13. When the confirmation dialog appears click on "Submit" 50 | -------------------------------------------------------------------------------- /test/ethusd.js: -------------------------------------------------------------------------------- 1 | var Promise = require("bluebird"); 2 | var getBalance = Promise.promisify(web3.eth.getBalance); 3 | var EthUSD = artifacts.require("./EthUSD.sol"); 4 | var MockEtherPriceOracle = artifacts.require("./MockEtherPriceOracle.sol"); 5 | 6 | contract('EthUSD', function(accounts) { 7 | var ethUSD; 8 | var mockEtherPriceOracle; 9 | 10 | beforeEach(async () => { 11 | mockEtherPriceOracle = await MockEtherPriceOracle.deployed() 12 | ethUSD = await EthUSD.new(MockEtherPriceOracle.address); 13 | }); 14 | 15 | describe("#issue", async () => { 16 | it("should update the user's balance", async () => { 17 | await mockEtherPriceOracle.setPrice(30000); 18 | await ethUSD.issue({value: web3.toWei('1', 'ether')}); 19 | const balance = await ethUSD.balanceOf(accounts[0]); 20 | assert.equal(balance.valueOf(), 30000); 21 | }); 22 | 23 | it("increments the total supply", async () => { 24 | await mockEtherPriceOracle.setPrice(30000); 25 | await ethUSD.issue({value: web3.toWei('1', 'ether')}); 26 | const totalSupply = await ethUSD.totalSupply.call(); 27 | assert.equal(totalSupply.toNumber(), 30000); 28 | }); 29 | }); 30 | 31 | describe("#withdraw", async () => { 32 | it("won't let you withdraw more EthUSD than you have", async () => { 33 | await mockEtherPriceOracle.setPrice(30000); 34 | await ethUSD.issue({value: web3.toWei('1', 'ether')}); 35 | try { 36 | await ethUSD.withdraw(30001); 37 | } catch (error) { 38 | assert.match(error, /VM Exception[a-zA-Z0-9 ]+: invalid opcode/); 39 | } 40 | }); 41 | 42 | it("decrements the users balance", async () => { 43 | await mockEtherPriceOracle.setPrice(30000); 44 | await ethUSD.issue({value: web3.toWei('1', 'ether')}); 45 | await ethUSD.withdraw(30000); 46 | const balance = await ethUSD.balanceOf(accounts[0]); 47 | assert.equal(balance.valueOf(), 0); 48 | }); 49 | 50 | it("decrements the total supply", async () => { 51 | await mockEtherPriceOracle.setPrice(30000); 52 | await ethUSD.issue({value: web3.toWei('1', 'ether')}); 53 | await ethUSD.withdraw(30000); 54 | const totalSupply = await ethUSD.totalSupply.call(); 55 | assert.equal(totalSupply.toNumber(), 0); 56 | }); 57 | 58 | it("if the price hasn't changed you should get back what you put in", async () => { 59 | await mockEtherPriceOracle.setPrice(30000); 60 | await ethUSD.issue({value: web3.toWei('1', 'ether')}); 61 | const amountWithdrawn = await ethUSD.withdraw.call(30000); 62 | assert.equal(amountWithdrawn.toNumber(), web3.toWei('1', 'ether')); 63 | }); 64 | 65 | it("if the price of Ether doubles you should get back half as much Ether (the same amount in USD)", async () => { 66 | await mockEtherPriceOracle.setPrice(30000); 67 | await ethUSD.issue({value: web3.toWei('1', 'ether')}); 68 | await mockEtherPriceOracle.setPrice(60000); 69 | const amountWithdrawn = await ethUSD.withdraw.call(30000); 70 | assert.equal(amountWithdrawn.toNumber(), web3.toWei('0.5', 'ether')); 71 | }); 72 | 73 | it("if the price of Ether gets cut in half and the contract can cover the your balance you should get back twice as much Ether (the same amount in USD)", async () => { 74 | await ethUSD.donate({value: web3.toWei('1', 'ether')}); 75 | await mockEtherPriceOracle.setPrice(30000); 76 | await ethUSD.issue({value: web3.toWei('1', 'ether')}); 77 | await mockEtherPriceOracle.setPrice(15000); 78 | const amountWithdrawn = await ethUSD.withdraw.call(30000); 79 | assert.equal(amountWithdrawn.toNumber(), web3.toWei('2', 'ether')); 80 | }); 81 | 82 | it("if the price of Ether gets cut in half and the contract can't cover the your balance you should get back half as much Ether", async () => { 83 | await mockEtherPriceOracle.setPrice(30000); 84 | await ethUSD.issue({value: web3.toWei('1', 'ether')}); 85 | await mockEtherPriceOracle.setPrice(15000); 86 | const amountWithdrawn = await ethUSD.withdraw.call(30000); 87 | assert.equal(amountWithdrawn.toNumber(), web3.toWei('1', 'ether')); 88 | }); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /EthUSD.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "constant": true, 4 | "inputs": [], 5 | "name": "name", 6 | "outputs": [ 7 | { 8 | "name": "", 9 | "type": "string" 10 | } 11 | ], 12 | "payable": false, 13 | "type": "function" 14 | }, 15 | { 16 | "constant": false, 17 | "inputs": [ 18 | { 19 | "name": "_spender", 20 | "type": "address" 21 | }, 22 | { 23 | "name": "_amount", 24 | "type": "uint256" 25 | } 26 | ], 27 | "name": "approve", 28 | "outputs": [ 29 | { 30 | "name": "success", 31 | "type": "bool" 32 | } 33 | ], 34 | "payable": false, 35 | "type": "function" 36 | }, 37 | { 38 | "constant": true, 39 | "inputs": [], 40 | "name": "totalSupply", 41 | "outputs": [ 42 | { 43 | "name": "", 44 | "type": "uint256" 45 | } 46 | ], 47 | "payable": false, 48 | "type": "function" 49 | }, 50 | { 51 | "constant": false, 52 | "inputs": [ 53 | { 54 | "name": "_from", 55 | "type": "address" 56 | }, 57 | { 58 | "name": "_to", 59 | "type": "address" 60 | }, 61 | { 62 | "name": "_amount", 63 | "type": "uint256" 64 | } 65 | ], 66 | "name": "transferFrom", 67 | "outputs": [ 68 | { 69 | "name": "success", 70 | "type": "bool" 71 | } 72 | ], 73 | "payable": false, 74 | "type": "function" 75 | }, 76 | { 77 | "constant": false, 78 | "inputs": [ 79 | { 80 | "name": "amountInCents", 81 | "type": "uint256" 82 | } 83 | ], 84 | "name": "withdraw", 85 | "outputs": [ 86 | { 87 | "name": "amountInWei", 88 | "type": "uint256" 89 | } 90 | ], 91 | "payable": false, 92 | "type": "function" 93 | }, 94 | { 95 | "constant": true, 96 | "inputs": [], 97 | "name": "decimals", 98 | "outputs": [ 99 | { 100 | "name": "", 101 | "type": "uint8" 102 | } 103 | ], 104 | "payable": false, 105 | "type": "function" 106 | }, 107 | { 108 | "constant": true, 109 | "inputs": [ 110 | { 111 | "name": "_owner", 112 | "type": "address" 113 | } 114 | ], 115 | "name": "balanceOf", 116 | "outputs": [ 117 | { 118 | "name": "balance", 119 | "type": "uint256" 120 | } 121 | ], 122 | "payable": false, 123 | "type": "function" 124 | }, 125 | { 126 | "constant": true, 127 | "inputs": [], 128 | "name": "symbol", 129 | "outputs": [ 130 | { 131 | "name": "", 132 | "type": "string" 133 | } 134 | ], 135 | "payable": false, 136 | "type": "function" 137 | }, 138 | { 139 | "constant": false, 140 | "inputs": [], 141 | "name": "getPrice", 142 | "outputs": [ 143 | { 144 | "name": "", 145 | "type": "uint256" 146 | } 147 | ], 148 | "payable": false, 149 | "type": "function" 150 | }, 151 | { 152 | "constant": false, 153 | "inputs": [ 154 | { 155 | "name": "_to", 156 | "type": "address" 157 | }, 158 | { 159 | "name": "_amount", 160 | "type": "uint256" 161 | } 162 | ], 163 | "name": "transfer", 164 | "outputs": [ 165 | { 166 | "name": "success", 167 | "type": "bool" 168 | } 169 | ], 170 | "payable": false, 171 | "type": "function" 172 | }, 173 | { 174 | "constant": false, 175 | "inputs": [], 176 | "name": "issue", 177 | "outputs": [], 178 | "payable": true, 179 | "type": "function" 180 | }, 181 | { 182 | "constant": true, 183 | "inputs": [ 184 | { 185 | "name": "_owner", 186 | "type": "address" 187 | }, 188 | { 189 | "name": "_spender", 190 | "type": "address" 191 | } 192 | ], 193 | "name": "allowance", 194 | "outputs": [ 195 | { 196 | "name": "remaining", 197 | "type": "uint256" 198 | } 199 | ], 200 | "payable": false, 201 | "type": "function" 202 | }, 203 | { 204 | "constant": false, 205 | "inputs": [], 206 | "name": "donate", 207 | "outputs": [], 208 | "payable": true, 209 | "type": "function" 210 | }, 211 | { 212 | "inputs": [ 213 | { 214 | "name": "etherPriceOracleAddress", 215 | "type": "address" 216 | } 217 | ], 218 | "payable": false, 219 | "type": "constructor" 220 | }, 221 | { 222 | "anonymous": false, 223 | "inputs": [ 224 | { 225 | "indexed": true, 226 | "name": "_from", 227 | "type": "address" 228 | }, 229 | { 230 | "indexed": true, 231 | "name": "_to", 232 | "type": "address" 233 | }, 234 | { 235 | "indexed": false, 236 | "name": "_value", 237 | "type": "uint256" 238 | } 239 | ], 240 | "name": "Transfer", 241 | "type": "event" 242 | }, 243 | { 244 | "anonymous": false, 245 | "inputs": [ 246 | { 247 | "indexed": true, 248 | "name": "_owner", 249 | "type": "address" 250 | }, 251 | { 252 | "indexed": true, 253 | "name": "_spender", 254 | "type": "address" 255 | }, 256 | { 257 | "indexed": false, 258 | "name": "_value", 259 | "type": "uint256" 260 | } 261 | ], 262 | "name": "Approval", 263 | "type": "event" 264 | } 265 | ] 266 | --------------------------------------------------------------------------------