Skip to content

Instantly share code, notes, and snippets.

@mpolci
Forked from blackyblack/escrow_goods.sol
Created July 10, 2016 07:29
Show Gist options
  • Select an option

  • Save mpolci/15acd19a8e5863f8335bd2133cf76f28 to your computer and use it in GitHub Desktop.

Select an option

Save mpolci/15acd19a8e5863f8335bd2133cf76f28 to your computer and use it in GitHub Desktop.

Revisions

  1. @blackyblack blackyblack created this gist Jul 5, 2016.
    316 changes: 316 additions & 0 deletions escrow_goods.sol
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,316 @@
    /* A contract to store goods with escrowed funds. */

    /* Deployment:
    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"}],"type":"function"},{"constant":true,"inputs":[],"name":"count","outputs":[{"name":"","type":"uint16"}],"type":"function"},{"constant":false,"inputs":[{"name":"datainfo","type":"string"},{"name":"_version","type":"uint256"}],"name":"cancel","outputs":[],"type":"function"},{"constant":true,"inputs":[],"name":"seller","outputs":[{"name":"","type":"address"}],"type":"function"},{"constant":true,"inputs":[],"name":"freezePeriod","outputs":[{"name":"","type":"uint256"}],"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":true,"inputs":[],"name":"status","outputs":[{"name":"","type":"uint16"}],"type":"function"},{"constant":true,"inputs":[],"name":"rewardPromille","outputs":[{"name":"","type":"uint256"}],"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":true,"inputs":[],"name":"totalEscrows","outputs":[{"name":"","type":"uint256"}],"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":"feeFunds","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"constant":false,"inputs":[{"name":"id","type":"uint256"},{"name":"datainfo","type":"string"},{"name":"_version","type":"uint256"}],"name":"yes","outputs":[],"type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"buyers","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"constant":true,"inputs":[],"name":"availableCount","outputs":[{"name":"","type":"uint16"}],"type":"function"},{"constant":true,"inputs":[],"name":"price","outputs":[{"name":"","type":"uint256"}],"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"},{"constant":true,"inputs":[],"name":"feePromille","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"constant":true,"inputs":[],"name":"activityTime","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"constant":true,"inputs":[],"name":"pendingCount","outputs":[{"name":"","type":"uint16"}],"type":"function"},{"constant":true,"inputs":[],"name":"arbiter","outputs":[{"name":"","type":"address"}],"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.3.5-2016-06-14-371690f
    */

    contract escrow_goods {

    //seller/owner of the goods
    address public seller;

    //escrow related

    struct EscrowInfo {
    address buyer;
    uint lockedFunds;
    uint frozenTime;
    uint frozenFunds;
    uint buyerNo;
    uint sellerNo;
    }

    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 constant arbitragePeriod = 30 days;
    uint constant stalePeriod = 180 days;

    uint public activityTime;
    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;

    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) _ }

    function escrow_goods(address _arbiter, uint _freezePeriod, uint _feePromille, uint _rewardPromille,
    uint16 _count, uint _price) {

    seller = msg.sender;

    //escrow related
    arbiter = _arbiter;
    freezePeriod = _freezePeriod;
    feePromille = _feePromille;
    rewardPromille = _rewardPromille;

    activityTime = now;
    feeFunds = 0;
    totalEscrows = 0;

    //goods related
    //status = Available
    status = 1;
    count = _count;
    price = _price;

    availableCount = count;
    pendingCount = 0;
    }

    function kill() {
    //allow anyone to claim dead contract funds
    if(now > (activityTime + stalePeriod)) {
    suicide(msg.sender);
    return;
    }

    //do not allow killing contract with active escrows
    if(totalEscrows > 0) return;
    //do not allow killing contract with unclaimed escrow fees
    if(feeFunds > 0) return;
    if(msg.sender != seller) return;
    suicide(seller);
    }

    function log(string message) private {
    log_event(message);
    }

    //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) return;
    if(msg.sender != info.buyer && msg.sender != seller) return;

    uint payment = info.lockedFunds;
    if(payment > this.balance) payment = this.balance;
    if(totalEscrows > 0) totalEscrows -= 1;
    info.lockedFunds = 0;

    if(msg.sender == info.buyer) {
    //send funds to seller
    seller.send(payment);
    }

    if(msg.sender == seller) {
    //send funds to buyer
    info.buyer.send(payment);
    }

    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) return;
    if(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;
    }

    if(msg.sender == seller) {
    info.sellerNo = 1;
    }

    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) return;
    if(info.frozenFunds == 0) return;

    if(who != seller && who != info.buyer) return;
    //requires both NO to arbitrage
    if(info.buyerNo == 0 || info.sellerNo == 0) return;

    if(info.lockedFunds > this.balance) info.lockedFunds = this.balance;
    if(payment > info.lockedFunds) payment = info.lockedFunds;

    //limit payment
    uint reward = (info.lockedFunds * rewardPromille) / 1000;
    if(reward > (info.lockedFunds - payment)) {
    log("Reward exceeds reward limit");
    return;
    }

    //send funds to the winner
    who.send(payment);

    //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) return;
    if(feeFunds > this.balance) feeFunds = this.balance;
    arbiter.send(feeFunds);
    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) return;
    //HACK: this check is necessary since frozenTime == 0 at escrow creation
    if(info.frozenFunds == 0) return;

    //timout for voting not over yet
    if(now < (info.frozenTime + freezePeriod)) return;

    uint payment = info.lockedFunds;
    if(payment > this.balance) payment = this.balance;
    //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)) return;

    //arbiter was silent so redeem the funds to the buyer
    info.buyer.send(payment);
    info.lockedFunds = 0;
    return;
    }

    if(info.buyerNo != 0) {
    info.buyer.send(payment);
    info.lockedFunds = 0;
    return;
    }
    if(info.sellerNo != 0) {
    seller.send(payment);
    info.lockedFunds = 0;
    return;
    }
    }

    //goods API

    //buy with escrow. id - escrow info id
    function buy(uint id, string datainfo, uint _version, uint16 _count) {
    if(status != 1) { log("status != 1"); throw; }
    if(msg.value < (price * _count)) { log("msg.value < (price * _count)"); throw; }
    if(_count > availableCount) { log("_count > availableCount"); throw; }

    //create default EscrowInfo struct or access existing
    EscrowInfo info = escrows[id];

    //lock only once for a given id
    if(info.lockedFunds > 0) {
    log("Already locked");
    throw;
    }

    //lock funds
    //refresh watchdog timer
    activityTime = now;

    uint fee = (msg.value * feePromille) / 1000;
    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("_count > availableCount"); return; }
    if(_count > pendingCount) { log("_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("_count > pendingCount"); return; }

    pendingCount -= _count;

    //send money back
    yes(id, datainfo, _version);

    //Reject order to event log
    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;
    }
    }