Skip to content

Instantly share code, notes, and snippets.

@blackyblack
Created August 15, 2016 15:56
Show Gist options
  • Save blackyblack/4bfb4266057610a2b63153d288ef0051 to your computer and use it in GitHub Desktop.
Save blackyblack/4bfb4266057610a2b63153d288ef0051 to your computer and use it in GitHub Desktop.
/* A contract to store goods with escrowed funds. */
/* Deployment:
Owner: seller
Last address: dynamic
Lib ABI: [{"constant":false,"inputs":[{"name":"data","type":"EscrowLib.Context storage"},{"name":"id","type":"uint256"},{"name":"datainfo","type":"string"},{"name":"_version","type":"uint256"},{"name":"_count","type":"uint16"}],"name":"buy","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"data","type":"EscrowLib.Context storage"}],"name":"getFees","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"data","type":"EscrowLib.Context storage"},{"name":"id","type":"uint256"},{"name":"datainfo","type":"string"},{"name":"_version","type":"uint256"}],"name":"yes","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"data","type":"EscrowLib.Context storage"},{"name":"datainfo","type":"string"},{"name":"_version","type":"uint256"}],"name":"cancel","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"data","type":"EscrowLib.Context storage"},{"name":"id","type":"uint256"},{"name":"datainfo","type":"string"},{"name":"_version","type":"uint256"},{"name":"_count","type":"uint16"},{"name":"recipient","type":"address"},{"name":"amount","type":"uint256"}],"name":"reject","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"data","type":"EscrowLib.Context storage"},{"name":"id","type":"uint256"},{"name":"datainfo","type":"string"},{"name":"_version","type":"uint256"},{"name":"_count","type":"uint16"}],"name":"accept","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"data","type":"EscrowLib.Context storage"},{"name":"id","type":"uint256"},{"name":"who","type":"address"},{"name":"payment","type":"uint256"},{"name":"datainfo","type":"string"},{"name":"_version","type":"uint256"}],"name":"arbYes","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"data","type":"EscrowLib.Context storage"},{"name":"id","type":"uint256"}],"name":"getMoney","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"data","type":"EscrowLib.Context storage"}],"name":"unbuy","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"data","type":"EscrowLib.Context storage"}],"name":"kill","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"data","type":"EscrowLib.Context storage"},{"name":"id","type":"uint256"},{"name":"datainfo","type":"string"},{"name":"_version","type":"uint256"}],"name":"no","outputs":[],"type":"function"},{"anonymous":false,"inputs":[{"indexed":false,"name":"message","type":"string"}],"name":"log_event","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"lockid","type":"uint256"},{"indexed":false,"name":"datainfo","type":"string"},{"indexed":true,"name":"version","type":"uint256"},{"indexed":false,"name":"datatype","type":"uint256"},{"indexed":true,"name":"sender","type":"address"},{"indexed":false,"name":"count","type":"uint256"},{"indexed":false,"name":"payment","type":"uint256"}],"name":"content","type":"event"}]
ABI: [{"constant":false,"inputs":[{"name":"datainfo","type":"string"},{"name":"_version","type":"uint256"}],"name":"cancel","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"id","type":"uint256"},{"name":"datainfo","type":"string"},{"name":"_version","type":"uint256"},{"name":"_count","type":"uint16"}],"name":"buy","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"id","type":"uint256"}],"name":"getMoney","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"id","type":"uint256"},{"name":"datainfo","type":"string"},{"name":"_version","type":"uint256"}],"name":"no","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"id","type":"uint256"},{"name":"datainfo","type":"string"},{"name":"_version","type":"uint256"},{"name":"_count","type":"uint16"},{"name":"recipient","type":"address"},{"name":"amount","type":"uint256"}],"name":"reject","outputs":[],"type":"function"},{"constant":false,"inputs":[],"name":"kill","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"id","type":"uint256"},{"name":"who","type":"address"},{"name":"payment","type":"uint256"},{"name":"datainfo","type":"string"},{"name":"_version","type":"uint256"}],"name":"arbYes","outputs":[],"type":"function"},{"constant":true,"inputs":[],"name":"data","outputs":[{"name":"seller","type":"address"},{"name":"arbiter","type":"address"},{"name":"freezePeriod","type":"uint256"},{"name":"feePromille","type":"uint256"},{"name":"rewardPromille","type":"uint256"},{"name":"activityTime","type":"uint256"},{"name":"feeFunds","type":"uint256"},{"name":"totalEscrows","type":"uint256"},{"name":"status","type":"uint16"},{"name":"count","type":"uint16"},{"name":"price","type":"uint256"},{"name":"availableCount","type":"uint16"},{"name":"pendingCount","type":"uint16"},{"name":"atomicLock","type":"uint16"}],"type":"function"},{"constant":false,"inputs":[{"name":"id","type":"uint256"},{"name":"datainfo","type":"string"},{"name":"_version","type":"uint256"}],"name":"yes","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"id","type":"uint256"},{"name":"datainfo","type":"string"},{"name":"_version","type":"uint256"},{"name":"_count","type":"uint16"}],"name":"accept","outputs":[],"type":"function"},{"constant":false,"inputs":[],"name":"unbuy","outputs":[],"type":"function"},{"constant":false,"inputs":[],"name":"getFees","outputs":[],"type":"function"},{"inputs":[{"name":"_arbiter","type":"address"},{"name":"_freezePeriod","type":"uint256"},{"name":"_feePromille","type":"uint256"},{"name":"_rewardPromille","type":"uint256"},{"name":"_count","type":"uint16"},{"name":"_price","type":"uint256"}],"type":"constructor"}]
Optimized: yes
Solidity version: v0.3.6-2016-08-12-9e03bda
*/
library EscrowLib {
struct EscrowInfo {
address buyer;
uint lockedFunds;
uint frozenTime;
uint frozenFunds;
uint buyerNo;
uint sellerNo;
}
uint constant arbitragePeriod = 30 days;
uint constant stalePeriod = 180 days;
uint constant safeGas = 25000;
struct Context {
//seller/owner of the goods
address seller;
//escrow related
address arbiter;
uint freezePeriod;
//each lock fee in promilles.
uint feePromille;
//reward in promilles. promille = percent * 10, eg 1,5% reward = 15 rewardPromille
uint rewardPromille;
uint activityTime;
uint feeFunds;
uint totalEscrows;
mapping (uint => EscrowInfo) escrows;
//goods related
//status of the goods: Available, Pending, Sold, Canceled
uint16 status;
//how many for sale
uint16 count;
//price per item
uint price;
uint16 availableCount;
uint16 pendingCount;
mapping (address => uint) buyers;
uint16 atomicLock;
}
event log_event(string message);
event content(uint indexed lockid, string datainfo, uint indexed version, uint datatype, address indexed sender, uint count, uint payment);
modifier onlyowner(Context storage data) { if (msg.sender == data.seller) _ }
function kill(Context storage data) {
//do not allow killing contract with active escrows
if(data.totalEscrows > 0) {
log_event("totalEscrows > 0");
return;
}
//do not allow killing contract with unclaimed escrow fees
if(data.feeFunds > 0) {
log_event("feeFunds > 0");
return;
}
//only allow the seller to kill contract
if(msg.sender != data.seller) {
log_event("msg.sender != data.seller");
return;
}
suicide(msg.sender);
}
function safeSend(Context storage data, address addr, uint value) internal returns (bool) {
if(data.atomicLock > 0) throw;
data.atomicLock = 1;
if (!(addr.call.gas(safeGas).value(value)())) {
data.atomicLock = 0;
return false;
}
data.atomicLock = 0;
return true;
}
//escrow API
//vote YES - immediately sends funds to the peer
function yes(Context storage data, uint id, string datainfo, uint _version) {
EscrowInfo info = data.escrows[id];
if(info.lockedFunds == 0) {
log_event("info.lockedFunds == 0");
return;
}
if(msg.sender != info.buyer && msg.sender != data.seller) {
log_event("msg.sender != info.buyer && msg.sender != data.seller");
return;
}
uint payment = info.lockedFunds;
if(payment > this.balance) {
//HACK: should not get here - funds cannot be unlocked in this case
log_event("payment > this.balance");
return;
}
if(msg.sender == info.buyer) {
//send funds to seller
if(!safeSend(data, data.seller, payment)) {
log_event("!safeSend(data, data.seller, payment)");
return;
}
} else if(msg.sender == data.seller) {
//send funds to buyer
if(!safeSend(data, info.buyer, payment)) {
log_event("!safeSend(data, info.buyer, payment)");
return;
}
} else {
//HACK: should not get here
log_event("unknown msg.sender");
return;
}
//remove record from escrows
if(data.totalEscrows > 0) data.totalEscrows -= 1;
info.lockedFunds = 0;
content(id, datainfo, _version, 11, msg.sender, 0, payment);
}
//vote NO - freeze funds for arbitrage
function no(Context storage data, uint id, string datainfo, uint _version) {
EscrowInfo info = data.escrows[id];
if(info.lockedFunds == 0) {
log_event("info.lockedFunds == 0");
return;
}
if(msg.sender != info.buyer && msg.sender != data.seller) {
log_event("msg.sender != info.buyer && msg.sender != data.seller");
return;
}
//freeze funds
//only allow one time freeze
if(info.frozenFunds == 0) {
info.frozenFunds = info.lockedFunds;
info.frozenTime = now;
}
if(msg.sender == info.buyer) {
info.buyerNo = 1;
}
else if(msg.sender == data.seller) {
info.sellerNo = 1;
} else {
//HACK: should not get here
log_event("unknown msg.sender");
return;
}
content(id, datainfo, _version, 12, msg.sender, 0, info.lockedFunds);
}
//arbiter's decision on the case.
//arbiter can only decide when both buyer and seller voted NO
//arbiter decides on his own reward but not bigger than announced percentage (rewardPromille)
function arbYes(Context storage data, uint id, address who, uint payment, string datainfo, uint _version) {
if(msg.sender != data.arbiter) return;
EscrowInfo info = data.escrows[id];
if(info.lockedFunds == 0) {
log_event("info.lockedFunds == 0");
return;
}
if(info.frozenFunds == 0) {
log_event("info.frozenFunds == 0");
return;
}
if(who != data.seller && who != info.buyer) {
log_event("who != data.seller && who != info.buyer");
return;
}
//requires both NO to arbitrage
if(info.buyerNo == 0 || info.sellerNo == 0) {
log_event("info.buyerNo == 0 || info.sellerNo == 0");
return;
}
if(payment > info.lockedFunds) {
log_event("payment > info.lockedFunds");
return;
}
if(payment > this.balance) {
//HACK: should not get here - funds cannot be unlocked in this case
log_event("payment > this.balance");
return;
}
//limit payment
uint reward = (info.lockedFunds * data.rewardPromille) / 1000;
if(reward > (info.lockedFunds - payment)) {
log_event("reward > (info.lockedFunds - payment)");
return;
}
//send funds to the winner
if(!safeSend(data, who, payment)) {
log_event("!safeSend(data, who, payment)");
return;
}
//send the rest as reward
info.lockedFunds -= payment;
data.feeFunds += info.lockedFunds;
info.lockedFunds = 0;
content(id, datainfo, _version, 13, msg.sender, 0, payment);
}
//allow arbiter to get his collected fees
function getFees(Context storage data) {
if(msg.sender != data.arbiter) {
log_event("msg.sender != data.arbiter");
return;
}
if(data.feeFunds > this.balance) {
//HACK: should not get here - funds cannot be unlocked in this case
log_event("data.feeFunds > this.balance");
return;
}
if(!safeSend(data, data.arbiter, data.feeFunds)) {
log_event("!safeSend(data, data.arbiter, data.feeFunds)");
return;
}
data.feeFunds = 0;
}
//allow buyer or seller take timeouted funds.
//buyer can get funds if seller is silent and seller can get funds if buyer is silent (after freezePeriod)
//buyer can get back funds under arbitrage if arbiter is silent (after arbitragePeriod)
function getMoney(Context storage data, uint id) {
EscrowInfo info = data.escrows[id];
if(info.lockedFunds == 0) {
log_event("info.lockedFunds == 0");
return;
}
//HACK: this check is necessary since frozenTime == 0 at escrow creation
if(info.frozenFunds == 0) {
log_event("info.frozenFunds == 0");
return;
}
//timout for voting not over yet
if(now < (info.frozenTime + data.freezePeriod)) {
log_event("now < (info.frozenTime + data.freezePeriod)");
return;
}
uint payment = info.lockedFunds;
if(payment > this.balance) {
//HACK: should not get here - funds cannot be unlocked in this case
log_event("payment > this.balance");
return;
}
//both has voted - money is under arbitrage
if(info.buyerNo != 0 && info.sellerNo != 0) {
//arbitrage timeout is not over yet
if(now < (info.frozenTime + data.freezePeriod + arbitragePeriod)) {
log_event("now < (info.frozenTime + data.freezePeriod + arbitragePeriod)");
return;
}
//arbiter was silent so redeem the funds to the buyer
if(!safeSend(data, info.buyer, payment)) {
log_event("!safeSend(data, info.buyer, payment)");
return;
}
info.lockedFunds = 0;
return;
}
if(info.buyerNo != 0) {
if(!safeSend(data, info.buyer, payment)) {
log_event("!safeSend(data, info.buyer, payment)");
return;
}
info.lockedFunds = 0;
return;
}
if(info.sellerNo != 0) {
if(!safeSend(data, data.seller, payment)) {
log_event("!safeSend(data, data.seller, payment)");
return;
}
info.lockedFunds = 0;
return;
}
}
//goods API
//buy with escrow. id - escrow info id
function buy(Context storage data, uint id, string datainfo, uint _version, uint16 _count) {
//reject money transfers for bad item status
if(data.status != 1) throw;
if(msg.value < (data.price * _count)) throw;
if(_count > data.availableCount) throw;
if(data.feePromille > 1000) throw;
if(data.rewardPromille > 1000) throw;
if((data.feePromille + data.rewardPromille) > 1000) throw;
//create default EscrowInfo struct or access existing
EscrowInfo info = data.escrows[id];
//lock only once for a given id
if(info.lockedFunds > 0) throw;
//lock funds
//refresh watchdog timer
data.activityTime = now;
uint fee = (msg.value * data.feePromille) / 1000;
//limit fees
if(fee > msg.value) throw;
uint funds = (msg.value - fee);
data.feeFunds += fee;
data.totalEscrows += 1;
info.buyer = msg.sender;
info.lockedFunds = funds;
info.frozenFunds = 0;
info.buyerNo = 0;
info.sellerNo = 0;
data.pendingCount += _count;
data.buyers[msg.sender] = 1;
//Buy order to event log
content(id, datainfo, _version, 1, msg.sender, _count, msg.value);
}
function accept(Context storage data, uint id, string datainfo, uint _version, uint16 _count) onlyowner(data) {
if(_count > data.availableCount) {
log_event("_count > availableCount");
return;
}
if(_count > data.pendingCount) {
log_event("_count > pendingCount");
return;
}
data.pendingCount -= _count;
data.availableCount -= _count;
//Accept order to event log
content(id, datainfo, _version, 2, msg.sender, _count, 0);
}
function reject(Context storage data, uint id, string datainfo, uint _version, uint16 _count, address recipient, uint amount) onlyowner(data) {
if(_count > data.pendingCount) {
log_event("_count > pendingCount");
return;
}
data.pendingCount -= _count;
//send money back
yes(data, id, datainfo, _version);
//Reject order to event log
//HACK: "yes" call above may fail and this event will be non-relevant. Do not rely on it.
content(id, datainfo, _version, 3, msg.sender, _count, amount);
}
function cancel(Context storage data, string datainfo, uint _version) onlyowner(data) {
//Canceled data.status
data.status = 2;
//Cancel order to event log
content(0, datainfo, _version, 4, msg.sender, data.availableCount, 0);
}
//remove buyer from the watchlist
function unbuy(Context storage data) {
data.buyers[msg.sender] = 0;
}
}
contract escrow_goods {
using EscrowLib for EscrowLib.Context;
EscrowLib.Context public data;
function escrow_goods(address _arbiter, uint _freezePeriod, uint _feePromille, uint _rewardPromille,
uint16 _count, uint _price) {
data.seller = msg.sender;
// all variables are always initialized to 0, save gas
//escrow related
data.arbiter = _arbiter;
data.freezePeriod = _freezePeriod;
data.feePromille = _feePromille;
data.rewardPromille = _rewardPromille;
data.activityTime = now;
//goods related
//status = Available
data.status = 1;
data.count = _count;
data.price = _price;
data.availableCount = data.count;
}
function kill() {
data.kill();
}
//escrow API
//vote YES - immediately sends funds to the peer
function yes(uint id, string datainfo, uint _version) {
data.yes(id, datainfo, _version);
}
//vote NO - freeze funds for arbitrage
function no(uint id, string datainfo, uint _version) {
data.no(id, datainfo, _version);
}
//arbiter's decision on the case.
//arbiter can only decide when both buyer and seller voted NO
//arbiter decides on his own reward but not bigger than announced percentage (rewardPromille)
function arbYes(uint id, address who, uint payment, string datainfo, uint _version) {
data.arbYes(id, who, payment, datainfo, _version);
}
//allow arbiter to get his collected fees
function getFees() {
data.getFees();
}
//allow buyer or seller take timeouted funds.
//buyer can get funds if seller is silent and seller can get funds if buyer is silent (after freezePeriod)
//buyer can get back funds under arbitrage if arbiter is silent (after arbitragePeriod)
function getMoney(uint id) {
data.getMoney(id);
}
//goods API
//buy with escrow. id - escrow info id
function buy(uint id, string datainfo, uint _version, uint16 _count) {
data.buy(id, datainfo, _version, _count);
}
function accept(uint id, string datainfo, uint _version, uint16 _count) {
data.accept(id, datainfo, _version, _count);
}
function reject(uint id, string datainfo, uint _version, uint16 _count, address recipient, uint amount) {
data.reject(id, datainfo, _version, _count, recipient, amount);
}
function cancel(string datainfo, uint _version) {
data.cancel(datainfo, _version);
}
//remove buyer from the watchlist
function unbuy() {
data.unbuy();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment