Skip to content

Instantly share code, notes, and snippets.

@blackyblack
Created September 14, 2016 11:46
Show Gist options
  • Save blackyblack/6296b50d21e8322774a40c6f0ad586a7 to your computer and use it in GitHub Desktop.
Save blackyblack/6296b50d21e8322774a40c6f0ad586a7 to your computer and use it in GitHub Desktop.
/* A contract to store goods with escrowed funds. */
/* Deployment:
Contract:
Owner: seller
Last address: dynamic
ABI: [{"constant":true,"inputs":[{"name":"","type":"uint256"}],"name":"escrows","outputs":[{"name":"buyer","type":"address"},{"name":"lockedFunds","type":"uint256"},{"name":"frozenTime","type":"uint256"},{"name":"frozenFunds","type":"uint256"},{"name":"buyerNo","type":"uint256"},{"name":"sellerNo","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"count","outputs":[{"name":"","type":"uint16"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"datainfo","type":"string"},{"name":"_version","type":"uint256"}],"name":"cancel","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"seller","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"freezePeriod","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"id","type":"uint256"},{"name":"datainfo","type":"string"},{"name":"_version","type":"uint256"},{"name":"_count","type":"uint16"}],"name":"buy","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"status","outputs":[{"name":"","type":"uint16"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"rewardPromille","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"id","type":"uint256"}],"name":"getMoney","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"id","type":"uint256"},{"name":"datainfo","type":"string"},{"name":"_version","type":"uint256"}],"name":"no","outputs":[],"payable":false,"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":[],"payable":false,"type":"function"},{"constant":false,"inputs":[],"name":"kill","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"totalEscrows","outputs":[{"name":"","type":"uint256"}],"payable":false,"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":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"feeFunds","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"id","type":"uint256"},{"name":"datainfo","type":"string"},{"name":"_version","type":"uint256"}],"name":"yes","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"buyers","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"availableCount","outputs":[{"name":"","type":"uint16"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"price","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"id","type":"uint256"},{"name":"datainfo","type":"string"},{"name":"_version","type":"uint256"},{"name":"_count","type":"uint16"}],"name":"accept","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[],"name":"unbuy","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[],"name":"getFees","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"feePromille","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"pendingCount","outputs":[{"name":"","type":"uint16"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"arbiter","outputs":[{"name":"","type":"address"}],"payable":false,"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"},{"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"}]
Optimized: yes
Solidity version: v0.4.2-nightly.2016.9.9
*/
pragma solidity ^0.4.0;
contract escrow_goods {
struct EscrowInfo {
address buyer;
uint lockedFunds;
uint frozenTime;
uint frozenFunds;
uint buyerNo;
uint sellerNo;
}
//data
uint constant arbitragePeriod = 30 days;
uint constant safeGas = 25000;
//seller/owner of the goods
address public seller;
//escrow related
address public arbiter;
uint public freezePeriod;
//each lock fee in promilles.
uint public feePromille;
//reward in promilles. promille = percent * 10, eg 1,5% reward = 15 rewardPromille
uint public rewardPromille;
uint public feeFunds;
uint public totalEscrows;
mapping (uint => EscrowInfo) public escrows;
//goods related
//status of the goods: Available, Pending, Sold, Canceled
uint16 public status;
//how many for sale
uint16 public count;
//price per item
uint public price;
uint16 public availableCount;
uint16 public pendingCount;
mapping (address => uint) public buyers;
uint16 atomicLock;
//events
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() { if (msg.sender == seller) _; }
//modules
function escrow_goods(address _arbiter, uint _freezePeriod, uint _feePromille, uint _rewardPromille,
uint16 _count, uint _price) {
seller = msg.sender;
// all variables are always initialized to 0, save gas
//escrow related
arbiter = _arbiter;
freezePeriod = _freezePeriod;
feePromille = _feePromille;
rewardPromille = _rewardPromille;
//goods related
//status = Available
status = 1;
count = _count;
price = _price;
availableCount = count;
}
function kill() {
//do not allow killing contract with active escrows
if(totalEscrows > 0) {
log_event("totalEscrows > 0");
return;
}
//do not allow killing contract with unclaimed escrow fees
if(feeFunds > 0) {
log_event("feeFunds > 0");
return;
}
//only allow the seller to kill contract
if(msg.sender != seller) {
log_event("msg.sender != seller");
return;
}
suicide(msg.sender);
}
function safeSend(address addr, uint value) internal returns (bool) {
if(atomicLock > 0) throw;
atomicLock = 1;
if (!(addr.call.gas(safeGas).value(value)())) {
atomicLock = 0;
return false;
}
atomicLock = 0;
return true;
}
//escrow API
//vote YES - immediately sends funds to the peer
function yes(uint id, string datainfo, uint _version) {
EscrowInfo info = escrows[id];
if(info.lockedFunds == 0) {
log_event("info.lockedFunds == 0");
return;
}
if(msg.sender != info.buyer && msg.sender != seller) {
log_event("msg.sender != info.buyer && msg.sender != 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(seller, payment)) {
log_event("!safeSend(seller, payment)");
return;
}
} else if(msg.sender == seller) {
//send funds to buyer
if(!safeSend(info.buyer, payment)) {
log_event("!safeSend(info.buyer, payment)");
return;
}
} else {
//HACK: should not get here
log_event("unknown msg.sender");
return;
}
//remove record from escrows
if(totalEscrows > 0) totalEscrows -= 1;
info.lockedFunds = 0;
content(id, datainfo, _version, 11, msg.sender, 0, payment);
}
//vote NO - freeze funds for arbitrage
function no(uint id, string datainfo, uint _version) {
EscrowInfo info = escrows[id];
if(info.lockedFunds == 0) {
log_event("info.lockedFunds == 0");
return;
}
if(msg.sender != info.buyer && msg.sender != seller) {
log_event("msg.sender != info.buyer && msg.sender != 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 == 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(uint id, address who, uint payment, string datainfo, uint _version) {
if(msg.sender != arbiter) return;
EscrowInfo info = escrows[id];
if(info.lockedFunds == 0) {
log_event("info.lockedFunds == 0");
return;
}
if(info.frozenFunds == 0) {
log_event("info.frozenFunds == 0");
return;
}
if(who != seller && who != info.buyer) {
log_event("who != 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 * rewardPromille) / 1000;
if(reward > (info.lockedFunds - payment)) {
log_event("reward > (info.lockedFunds - payment)");
return;
}
//send funds to the winner
if(!safeSend(who, payment)) {
log_event("!safeSend(who, payment)");
return;
}
//send the rest as reward
info.lockedFunds -= payment;
feeFunds += info.lockedFunds;
info.lockedFunds = 0;
content(id, datainfo, _version, 13, msg.sender, 0, payment);
}
//allow arbiter to get his collected fees
function getFees() {
if(msg.sender != arbiter) {
log_event("msg.sender != arbiter");
return;
}
if(feeFunds > this.balance) {
//HACK: should not get here - funds cannot be unlocked in this case
log_event("feeFunds > this.balance");
return;
}
if(!safeSend(arbiter, feeFunds)) {
log_event("!safeSend(arbiter, feeFunds)");
return;
}
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(uint id) {
EscrowInfo info = 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 + freezePeriod)) {
log_event("now < (info.frozenTime + 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 + freezePeriod + arbitragePeriod)) {
log_event("now < (info.frozenTime + freezePeriod + arbitragePeriod)");
return;
}
//arbiter was silent so redeem the funds to the buyer
if(!safeSend(info.buyer, payment)) {
log_event("!safeSend(info.buyer, payment)");
return;
}
info.lockedFunds = 0;
return;
}
if(info.buyerNo != 0) {
if(!safeSend(info.buyer, payment)) {
log_event("!safeSend(info.buyer, payment)");
return;
}
info.lockedFunds = 0;
return;
}
if(info.sellerNo != 0) {
if(!safeSend(seller, payment)) {
log_event("!safeSend(seller, payment)");
return;
}
info.lockedFunds = 0;
return;
}
}
//goods API
//buy with escrow. id - escrow info id
function buy(uint id, string datainfo, uint _version, uint16 _count) {
//reject money transfers for bad item status
if(status != 1) throw;
if(msg.value < (price * _count)) throw;
if(_count > availableCount) throw;
if(feePromille > 1000) throw;
if(rewardPromille > 1000) throw;
if((feePromille + rewardPromille) > 1000) throw;
//create default EscrowInfo struct or access existing
EscrowInfo info = escrows[id];
//lock only once for a given id
if(info.lockedFunds > 0) throw;
//lock funds
uint fee = (msg.value * feePromille) / 1000;
//limit fees
if(fee > msg.value) throw;
uint funds = (msg.value - fee);
feeFunds += fee;
totalEscrows += 1;
info.buyer = msg.sender;
info.lockedFunds = funds;
info.frozenFunds = 0;
info.buyerNo = 0;
info.sellerNo = 0;
pendingCount += _count;
buyers[msg.sender] = 1;
//Buy order to event log
content(id, datainfo, _version, 1, msg.sender, _count, msg.value);
}
function accept(uint id, string datainfo, uint _version, uint16 _count) onlyowner {
if(_count > availableCount) {
log_event("_count > availableCount");
return;
}
if(_count > pendingCount) {
log_event("_count > pendingCount");
return;
}
pendingCount -= _count;
availableCount -= _count;
//Accept order to event log
content(id, datainfo, _version, 2, msg.sender, _count, 0);
}
function reject(uint id, string datainfo, uint _version, uint16 _count, address recipient, uint amount) onlyowner {
if(_count > pendingCount) {
log_event("_count > pendingCount");
return;
}
pendingCount -= _count;
//send money back
yes(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(string datainfo, uint _version) onlyowner {
//Canceled status
status = 2;
//Cancel order to event log
content(0, datainfo, _version, 4, msg.sender, availableCount, 0);
}
//remove buyer from the watchlist
function unbuy() {
buyers[msg.sender] = 0;
}
}
@mpolci
Copy link

mpolci commented Sep 15, 2016

I saw pragma solidity ^0.4.0;. Good thing to support latest solidity release.
Check here the changes for solidity 0.4.0, in particular now you have to declare when a function can receive ethers with the payable modifier, otherwise the function throws if not specified and receive some ether. I think the function buy needs it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment