Commit 2544c81a authored by didi's avatar didi
Browse files

a bit of cleanup and adapted README for publishing to github

parent 5d666b68
**WARNING: This code is NOT intended or ready for production use. Bad things will happen if you use it as is!**
## About
The idea is described in more detail [here](https://cloud.lab10.io/s/Z3cKDm4asf4BPdc).
This is the first PoC implementation of Streems (read more about it [here](https://artis.eco/en/blog/detail/streaming-money-1)).
It includes a contract, a minimal web interface and an interactive test web interface.
Note that most of this was written in mid 2017, thus targets Solidity v0.4.13.
It still compiles (last version tested: v0.4.23), but with a lot of warnings. That's because Solidity has become a safer language since, allowing for and also demanding more explicit expression of intentions. See [Solidity Changelog](https://github.com/ethereum/solidity/blob/develop/Changelog.md).
The goal of this code was to proof feasibilty of continuous transfers (a kind of on-chain value streaming).
Core logic is in ` Streem.sol` which implements a minimal [ERC20 token](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md) (to be more precise - a subset of it as it lacks implementation of `transferFrom()`, `approve()` and `allowance()`).
The contracts allow only one outgoing and one incoming Streem per account in order to keep things simple.
This has the side effect of making **nested cycles** impossible to build up.
**Simple cycles** are supposed to be handled correctly by this implementation (no proof given though).
This approach was not further pursued, because I found no simple solution for how to avoid the building up of uncomputable dependency graphs.
While Streems can be chained, such chains make computing the balance at the end of such a Streem chain expensive (as it recurses through that chain) - to a point where it could exceed the block gas limit and thus become effectively uncomputable on-chain. Which would also mean that the last Streem of such a chain couldn't be closed anymore, because closing requires on-chain computation of the balance.
Since I didn't find a satisfying solution to this problem, this PoC wasn't developed further, instead we switched focus the concept of **Basic Streems** (as described [here](https://artis.eco/en/blog/detail/streaming-money-2)).
We also decided to limit Solidity implementations to PoCs and testing purposes and started a native implementation of Basic Streems in [Parity](https://github.com/paritytech/parity).
Such a native implementation allows for considerably reduced transaction costs and makes the basic unit of Account (ETH/ATS) streamable without wrapper.
There's no conventional tests ([like e.g. here](https://github.com/lab10-coop/Play4Privacy/tree/master/blockchain/test)) included. Instead I used a kind of interactive test app in `test.[html|js]` in order to quickly iterate various cases coming to my mind.
## Status
## How to run
PoC of the concept including an ERC-20 compatible token contract and a minimal web interface for opening, closing and observing streams.
Needs [truffle](http://truffleframework.com/) ganache-cli installed: `npm install -g truffle ganache-cli`
Start dev chain: `ganache-cli`
Then, in another tab, enter the backend directory and run `./deploy.sh` which will deploy the contract to the dev testnet and update frontend bindings.
TODO: Add watcher option.
## Next
You can now interact with the contract in various ways
* using `truffle console`
* using [remix](http://remix.ethereum.org/) (connect it to the local testnet, paste the contract code and load the contract at the deployed address)
* with the web app at `streem.html`
* with the interactive test web app at `test.html`
Next steps would be:
* Make the contract multi-stream capable (accounts can have both multiple instreams and outstreams). This probably requires something like `mapping (address => (uint => Stream)) [in|out]streams`.
* How can existing streams be referenced from outside? Should it be possible to name them when opening?
* Decide on a set of possible semantics and design the interface accordingly. E.g.
* Streams which auto-close when running out of funds
* Streams which guarantee a specified per-warning time before running out of stream (blocking outgoing transfers if balance too low)
* Streams with close date already predetermined
* Advanced: Streams depending on oracle data (dependents: speed, open state)
* Streams which can be closed by the receiver (should probably always be allowed)
* Events
* Check interoperability with various ERC-20 wallets.
* Figure out the issuance and governance mechanics and implement them.
Further a lot of attention needs to go into robustness of the contracts (special cases, error handling, gas economics).
In order to use the web apps, you need a webserver which can serve the static content in frontend directory.
Now you can point a browser to `streem.html` or `test.html`. In order to have the Apps connect to the locally running ganache-cli, the browser should not inject web3 (e.g. if you have Metamask installed, disabled it).
When enough is known, consider the overall contract architecture (e.g. a token contract and an issuance contract). Should anything be upgradable? Any kind of emergeny mechanism?
\ No newline at end of file
The contract can of course also be deployed to a public testnet (there's for example a [deployment on Rinkeby](https://rinkeby.etherscan.io/address/0xf73f6bd052061bb84913be57d5f7565b0aa38827)). In order to access such a contract via the web frontend, hardcode the address in the Javascript file and open it in a browser with web3 injected and connected to the respective network.
\ No newline at end of file
# How to run
Needs truffle and testrpc installed: `npm install -g truffle ethereumjs-testrpc`
Start testrpc: `testrpc`
Then, in another tab run `./contract-changed.sh` whenever the contract should be re-compiled JS bindings updated.
TODO: Add watcher option.
# The dynamic challenge
When checking the balance of a receiving address or closing an incoming stream, it's not enough to just look at the static balance of the sender.
**Sender has incoming stream(s)**: In this case looking at only staticBalance may return a too low result and lead to unnecessary drying of the stream.
**Sender has outgoing stream(s)**: In this case looking at only staticBalance may return a too high result, leading to a drying stream not being noticed.
Also, when doing a discrete transfer, it may now happen that the staticBalance is smaller than balance. Thus the amount can't just be subtracted from staticBalance.
Can we make staticBalance an int or would that introduce other issues?
## Options
### Only one stream per account
In this case an account could have only either an outgoing or an incoming stream.
Thus a receiver could rely on the sender not having any other stream, thus looking at staticBalance alone would be enough.
### Max one incoming and one outgoing stream
In this case a receiver could rely on the sender not having any other stream draining staticBalance.
Still she'd need for account for a potential parallel incoming stream.
Problem: there could be a circular relationship. E.g. A streams to B and B streams to A.
How to implement this without the potential for an endless recursion?
* Avoid creation of such a constellation (e.g. have *openStream()* check it)
* Find a way to implement it safely (should be possible, but I don't yet have an idea how)
Either way, we still need to protect from the risk of running out of gas due to many dependencies.
A possible solution could be a kind of maintenance cronjob which regularly creates kind of snapshots which cut down the dependencies on other streams.
That could be achieved by some kind of transient staticBalance reflecting a snapshot (what if all streams were closed at this point in time).
Such a value would increase the probability of receivers not needing to check incoming streams of the sender in case the transient staticBalance minus outgoing streams remained above the required threshold.
However for calculating the remaining runway for a stream, this may not help (?) - (does the gas limit apply for local execution?)
### Limited number of streams per account
This could probably be implemented with arrays.
Would need a clear strategy (or multiple) for how to deal with dry streams - similar to how insolvencies are dealt with.
Basic strategies are:
* first come first serve (e.g. older streams are served first)
* proportional: needs to know the point in time starting from which streams are underfunded / dry
* priority classes: could be combined with either the first come first serve or proportional strategy
Guess: Allowing 2 incoming and 2 outgoing streams would allow to construct all compositions possible with arbitrary number of streams by using *intermediate* accounts for aggregation or splitting.
### Arbitrary number of streams per account
Logically identical to the *limited number of streams* option.
But probably a considerably bigger implementation challenge in terms of complexity and gas cost. E.g. fixed size array nomore an option.
A possibility could be to have *openStream()* measure the complexity of the dependencies based on the involved addresses and not execute if it exceeds some safety threshold.
## Basic strategy
Whenever a transaction takes place, it can be used for some bookkeeping.
Most importantly, a kind of intermediate settlement can be done for open streams. This can be seen as persisting the results of calculations.
Since dynamically calculated status snapshots depending on the current time are guaranteed / final, such states can as well be persisted.
......@@ -55,7 +55,7 @@ contract Streem {
// ERC-20 compliant function for discrete transfers
// TODO: the standard seems to require bool return value
function transfer(address _to, uint256 _value) {
function transfer(address _to, uint256 _value) returns (bool) {
assert(_value > 0 && balanceOf(msg.sender) >= _value);
// if the settled balance doesn't suffice, settle the available funds of the ingoing stream.
......@@ -73,6 +73,7 @@ contract Streem {
settledBalances[msg.sender] -= int(_value);
settledBalances[_to] += int(_value);
Transfer(msg.sender, _to, _value);
return true;
}
/*
......@@ -134,10 +135,10 @@ contract Streem {
settledBalances[s.sender] -= int(settleBal);
settledBalances[s.receiver] += int(settleBal); // inS.receiver == msg.sender
// TODO: make sure we really don't need an extra field for this intermediate settlement.
// TODO: make sure we don't need an extra field if invoking this for open streams.
// For correct behaviour, it's irrelevant what the start time of the stream is.
// Applications can rely on the StreamOpened-Event for the UI.
// Still, the field may need a name better reflecting this flexible use.
// Still, the field may need a name better reflecting this semantics.
s.startTimestamp += dt;
// TODO: disable checks in prod if they cost gas and the logic is proofed
assert(s.startTimestamp <= now);
......@@ -154,7 +155,7 @@ contract Streem {
// ################## Internal constant functions ###################
// Solidity (so far) has no simple null check, using startTimestamp as guard (assuming 1970 will not come back).
// Solidity (so far) has no simple null check, using startTimestamp as guard (assuming we'll not overflow back to 1970).
function exists(Stream s) internal constant returns (bool) {
return s.startTimestamp != 0;
}
......@@ -187,13 +188,12 @@ contract Streem {
* returns the "real" (based on sender solvency) balance of a stream.
* This takes the perspective of the sender, making the stream under investigation an outgoingStream.
* Implements min(outgoingStreamBalance, staticBalance + incomingStreamBalance)
* TODO: due to the involved recursion, this will lead to an endless loop in circular relations, e.g. A -> B, B -> A
*/
function streamBalance(Stream s, Stream origin, uint hops) internal constant returns (uint256) {
// naming: osb -> outgoingStreamBalance, isb -> incomingStreamBalance, sb -> static balance
uint256 osb = naiveStreamBalance(s);
if (equals(s, origin) && hops > 1) { // special case: break on circular dependency. TODO: proof correctness
if (equals(s, origin) && hops > 1) { // special case: stop when detecting a cycle. TODO: proof correctness
return osb;
} else {
var inS = getInStreamOf(s.sender);
......@@ -210,7 +210,7 @@ contract Streem {
// ####################### dev / testing helpers #########################
// TODO: this is just for the test token. Issuance mechanism for mainnet token to be decided.
// TODO: this is just for the test token
function dev_issueTo(address receiver, uint256 amount) {
require(msg.sender == owner);
settledBalances[receiver] += int(amount);
......
pragma solidity ^0.4.13;
import "./Streem.sol";
// inspired by https://github.com/mattdf/payment-channel/blob/master/channel.sol
// just the interface we need
// TODO: use "interface" keyword (with solc 0.4.15)
contract IERC20Token {
function balanceOf(address _owner) constant returns (uint balance);
function transfer(address _to, uint _value) returns (bool success);
}
// TransferAccount instances hold the deposits of tokens converted to streamable tokens.
// Dumb contract which only contains a proxy transfer function
// also see https://github.com/bancorprotocol/contracts/blob/master/solidity/contracts/TokenHolder.sol
contract TransferAccount {
address owner;
function TransferAccount() {
owner = msg.sender;
}
// this just forwards a transfer request. Needs to be done by the contract itself in order to have msg.sender correct.
// TODO: could this also take an ERC20 object as param instead of address?
function transfer(address tokenAddr, address receiver, uint256 amount) returns (bool) {
assert(msg.sender == owner);
var token = IERC20Token(tokenAddr);
return token.transfer(receiver, amount);
}
}
// one instance represents one ERC20 token, the rest is very similar to the normal Streem contract.
// a model where a contract exists per user would offer more implicit security, but be less efficient (?)
contract StreemERC20 is Streem {
event Withdrawal(address sender, uint amount);
address owner;
IERC20Token token;
address tokenAddr;
mapping (address => TransferAccount) transferAccounts;
event TransferAccountCreated(address forAccount, address transferAddr);
// constructor. TODO: dynamically configure
function StreemERC20(address _tokenAddr) Streem(0, "Streaming Token", "STOK", 0) {
tokenAddr = tokenAddr;
token = IERC20Token(_tokenAddr);
}
// ################## Public functions ###################
function createTransferAccount() returns (address) {
var tAcc = new TransferAccount();
transferAccounts[msg.sender] = tAcc;
TransferAccountCreated(msg.sender, tAcc);
return address(transferAccounts[msg.sender]);
}
// override
function transfer(address _to, uint256 _value) {
poolTransferAccountFor(msg.sender);
super.transfer(_to, _value);
}
// override
function openStream(address receiver, uint256 perSecond) {
poolTransferAccountFor(msg.sender);
super.openStream(receiver, perSecond);
}
// override
function closeStream() {
poolTransferAccountFor(msg.sender);
super.closeStream();
}
// withdrawal of owner tokens
function withdraw(uint256 amount) {
poolTransferAccountFor(msg.sender);
// following the Checks-Effects-Interaction Pattern
assert(balanceOf(msg.sender) >= amount);
settledBalances[msg.sender] -= int(amount);
totalSupply -= amount;
assert(token.transfer(msg.sender, amount));
Withdrawal(msg.sender, amount);
}
// ################## Public constant functions ###################
function tokenAddress() constant returns(address) {
return address(token);
}
function getTransferAccount() constant returns (address) {
return transferAccounts[msg.sender];
}
// override
function balanceOf(address addr) constant returns (uint256) {
var baseBal = super.balanceOf(addr);
var transferAddr = address(transferAccounts[addr]);
if(transferAddr != 0) {
return baseBal + token.balanceOf(transferAddr);
} else {
return baseBal;
}
}
// ################## Internal functions ###################
// If there's funds in an associated transfer account, transfer it to this contract (pool)
// returns the amount moved over
function poolTransferAccountFor(address addr) internal returns(uint) {
if(address(transferAccounts[addr]) != 0) {
var tBal = token.balanceOf(address(transferAccounts[addr]));
if(tBal >= 0) {
// TODO: is assert a reasonable strategy here?
assert(transferAccounts[addr].transfer(address(token), this, tBal));
totalSupply += tBal;
settledBalances[addr] += int(tBal);
return tBal;
}
}
return 0;
}
}
\ No newline at end of file
pragma solidity ^0.4.13;
import "./Streem.sol";
contract StreemETH is Streem {
event Deposit(address sender, uint amount);
event Withdrawal(address sender, uint amount);
// constructor: just call the base constructor with the right args
function StreemETH() Streem(0, "Streaming Ether", "SETH", 18) {}
// conversion from StreemETH to ETH
function withdraw(uint256 amount) {
// following the Checks-Effects-Interaction Pattern
assert(balanceOf(msg.sender) >= amount);
settledBalances[msg.sender] -= int(amount);
totalSupply -= amount;
msg.sender.transfer(amount);
Withdrawal(msg.sender, amount);
}
// conversion from ETH to StreemETH
function() payable {
settledBalances[msg.sender] += int(msg.value);
totalSupply += msg.value;
Deposit(msg.sender, msg.value);
}
}
\ No newline at end of file
#!/bin/bash
# Quick'n dirty way to deploy the last version of the contract and keep frontend bindings in sync (ABI and address)
# Invokes 'truffle migrate' (compile, re-deploy)
# and tells the node script in frontend to update the JS file representing the contract
......@@ -10,5 +11,5 @@ set -u
truffle migrate --reset
node apply_contract_update.js build/contracts/Streem.json ../frontend/js/streem_contract.js
node apply_contract_update.js build/contracts/StreemETH.json ../frontend/js/streemETH_contract.js
#node apply_contract_update.js build/contracts/StreemETH.json ../frontend/js/streemETH_contract.js
echo "update applied"
var Streem = artifacts.require("./Streem.sol");
//var Streemify = artifacts.require("./Streemify.sol");
var StreemETH = artifacts.require("./StreemETH.sol");
//var StreemETH = artifacts.require("./StreemETH.sol");
module.exports = function(deployer) {
deployer.deploy([
[Streem, 100000000], // the second param is for the contract constructor
StreemETH
]);
// deployer.deploy(Streemify);
//deployer.deploy(StreemETH);
[Streem, 100000000, "STREEM", "STR", 0]
// StreemETH
]);
};
pragma solidity ^0.4.2;
import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../contracts/Streem.sol";
contract TestStreem {
address owner;
function testInitialBalanceUsingDeployedContract() {
owner = tx.origin;
Streem streem = Streem(DeployedAddresses.Streem());
uint expected = 10000;
//// issue some tokens to the owner
//streem.dev_issueTo(owner, expected);
//streem.dev_issueTo(tx.origin, 10000);
//uint balance = streem.balanceOf(tx.origin);
Assert.equal(streem.balanceOf(tx.origin), expected, "Owner should have 10000 STR initially");
}
function testTotalSupplyWithNewContract() {
uint expected = 10000;
Streem streem = new Streem(expected, "UnitTestStreem", "TEST-STR", 0);
Assert.equal(streem.totalSupply(), expected, "totalSupply should be 10000");
}
// TODO: test actual streaming functionality
}
......@@ -4,16 +4,6 @@ module.exports = {
host: "localhost",
port: 8545,
network_id: "*" // Match any network id
},
rinkeby: {
host: "rinkeby.eth.lab10.io", // Connect to geth on the specified
port: 80,
//from: "0xc6aa5459ef1cbbc4dce38c7bba5e01fd12b521a4", // default address to use for any transaction Truffle makes during migrations
from: "0xc6AA5459eF1CBBc4DcE38C7bBa5E01Fd12B521a4", // default address to use for any transaction Truffle makes during migrations
// needed to be unlocked permanently (timeout 0) in geth: personal.unlockAccount("0xc6aa5459ef1cbbc4dce38c7bba5e01fd12b521a4", "bernhard", 0)
network_id: 4,
// gas: 4612388 // Gas limit used for deploys
gas: 4700000 // Gas limit used for deploys
}
}
};
......@@ -4,13 +4,14 @@ function initContract() {
const abi = contract.abi;
// get address - in case of multiple network entries, the last seems to be a safe bet
/*
for(let networkId in contract.networks) {
//console.log(contract.networks[networkId])
var address = contract.networks[networkId].address
}
*/
var address = '0xf73f6bd052061bb84913be57d5f7565b0aa38827'
// in order to hardcode an address (e.g. of a public testnet),
// comment out the loop above, set the address in next line and uncomment
// var address = '0xf73f6bd052061bb84913be57d5f7565b0aa38827' // rinkeby deployment
var Web3 = require('web3');
......@@ -68,6 +69,12 @@ function initContract() {
})
}
/*
NOTE: gas amounts for contract calls are hardcoded to an arbitrary value.
Should instead be inferred via gas estimation upfront.
*/
const GASLIMIT = 200000
function onOpenStreamButton() {
console.log("open stream clicked")
......@@ -75,23 +82,24 @@ function onOpenStreamButton() {
const speed = document.getElementById('speed').value
console.log(`opening stream to ${rcv} with speed ${speed}`)
streem.openStream(rcv, speed, {gas: 200000}, txHandler) // TODO: gas is just a guess to make it working on testrpc
streem.openStream(rcv, speed, {gas: GASLIMIT}, txHandler)
}
// TODO: first test locally if it can be executed
function onCloseStreamButton() {
console.log("close stream clicked")
streem.closeStream({gas: 200000}, txHandler) // TODO: gas is just a guess to make it working on testrpc
streem.closeStream({gas: GASLIMIT}, txHandler)
}
function onTransferButton() {
console.log("transfer clicked")
const rcv = document.getElementById('transfer-receiver').value
const amount = document.getElementById('transfer-amount').value
streem.transfer(rcv, amount, {gas: 200000}, txHandler) // TODO: gas is just a guess to make it working on testrpc
streem.transfer(rcv, amount, {gas: GASLIMIT}, txHandler)
}
// TODO: how to detect failure of a transaction? See https://ethereum.stackexchange.com/questions/6007/how-can-the-transaction-status-from-a-thrown-error-be-detected-when-gas-can-be-e
// update: this was written pre-byzantium. TBD with check of status field now
function txHandler(err, txHash) {
if(err) {
console.log(`transfer failed: ${err}`)
......
// for contract metadata (format as produced by truffle)
function initContract() {
return new Promise( (resolve, reject) => {
const abi = contract.abi;
// get address - in case of multiple network entries, the last seems to be a safe bet
/*
for(let networkId in contract.networks) {
//console.log(contract.networks[networkId])
var address = contract.networks[networkId].address
}
*/
window.address = '0x03d675a91c375c0cede9bacc8add46824ee7b483'
var Web3 = require('web3');
if (typeof web3 !== 'undefined') {
// Mist / Metamask
console.log('web3 connects to provider')
web3 = new Web3(web3.currentProvider);
} else {
// standalone
//alert("This Dapp needs web3 injected (e.g. through the Metamask plugin.");
console.log('web3 connects to rpc')
web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"))
}
web3.eth.getAccounts((err, ret) => {
if (!err) web3.eth.defaultAccount = ret[0]
if (web3.eth.defaultAccount == undefined) {
alert("no Ethereum account found")
} else {
console.log(`defaultAccount: ${web3.eth.defaultAccount}`)
// contract is the contract template based on our abi
const StreemETH = web3.eth.contract(abi);
streemETH = StreemETH.at(address);
streemETH.totalSupply( (err, ret) => {
if(err/* || ret == 0*/) { // TODO: find another way to test existence
alert(`Cannot communicate with contract at given address ${address}`)
} else {
console.log(`contract loaded at ${address}`)
web3.version.getNetwork( (err, ret) => {
resolve({ contractaddr: address, networkid: ret})
})
}
})
}
})
// callback on block advance
web3.eth.filter('latest').watch((err, hash) => {
console.log(`new block: ${hash}`)
if (typeof onNextBlock == 'function') {
onNextBlock(hash)
}
})
// callback on pending transactions
/*
// the address filter seems not to work
web3.eth.filter({address: web3.eth.defaultAccount}).watch((err, ret) => {
console.log(`filtered event: ${JSON.stringify(ret)}`)
getReceipt(ret.transactionHash)
})
*/
})
}
function onOpenStreamButton() {
console.log("open stream clicked")
const rcv = document.getElementById('receiver').value
const speed = document.getElementById('speed').value
console.log(`opening stream to ${rcv} with speed ${speed}`)
streemETH.openStream(rcv, web3.toWei(speed), {gas: 200000}, txHandler) // TODO: gas is just a guess to make it working on testrpc
}
// TODO: first test locally if it can be executed
function onCloseStreamButton() {
console.log("close stream clicked")
streemETH.closeStream({gas: 200000}, txHandler) // TODO: gas is just a guess to make it working on testrpc
}
function onWithdrawButton() {
console.log("withdraw clicked")
const amount = document.getElementById('withdraw-amount').value
streemETH.withdraw(web3.toWei(amount), {gas: 200000}, txHandler) // TODO: gas is just a guess to make it working on testrpc
}
function onDepositButton() {
console.log("deposit clicked")
const amount = document.getElementById('deposit-amount').value
console.log(`sending ${web3.toWei(amount)} wei to ${window.address}`)
web3.eth.sendTransaction({to: window.address, value: web3.toWei(amount), gas: 200000}, txHandler) // TODO: gas is just a guess to make it working on testrpc
}
// TODO: how to detect failure of a transaction? See https://ethereum.stackexchange.com/questions/6007/how-can-the-transaction-status-from-a-thrown-error-be-detected-when-gas-can-be-e
function txHandler(err, txHash) {
if(err) {
console.log(`transfer failed: ${err}`)
alert(`transaction failed: ${err}`)
} else {
console.log(`new pending transaction: ${txHash}`)
window.pendingTx = txHash
document.getElementById('pendingtx').innerHTML = `<a href="${network.explorer}/tx/${txHash}" target="_blank">${txHash}</a>`
function incrementCounter(startTime) {
window.setTimeout(() => {
document.getElementById('pendingtxcounter').innerHTML = Math.floor(Date.now() / 1000) - startTime
if(window.pendingTx) {
incrementCounter(startTime)
} else {
document.getElementById('pendingtxcounter').innerHTML = ''
}
}, 1000)
}
incrementCounter(Math.floor(Date.now() / 1000))
}
}
function getReceipt(txHash) {
console.log(`getReceipt for tx ${txHash}`)