├── .gitignore ├── .gitmodules ├── Dappfile ├── LICENSE ├── README.md ├── src ├── btc_market.sol └── btc_market_test.sol └── todo.md /.gitignore: -------------------------------------------------------------------------------- 1 | /out 2 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/btc-tx"] 2 | path = lib/btc-tx 3 | url = https://github.com/rainbeam/solidity-btc-parser 4 | [submodule "lib/erc20"] 5 | path = lib/erc20 6 | url = https://github.com/dapphub/erc20 7 | [submodule "lib/ds-token"] 8 | path = lib/ds-token 9 | url = https://github.com/dapphub/ds-token 10 | -------------------------------------------------------------------------------- /Dappfile: -------------------------------------------------------------------------------- 1 | name btc-market 2 | description Trade ERC20 tokens for Bitcoin using BTC-Relay 3 | version 0.1.0 4 | author Rain 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 rain 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bitcoin <-> Token market 2 | 3 | Trade Bitcoin for Standard Tokens on Ethereum. 4 | 5 | ## Usage 6 | 7 | Alice wants to sell some ETH for bitcoins. She will accept delivery 8 | at her bitcoin address `1SendBitcoinsHerePlease`. 9 | 10 | Alice makes an offer to trade 30 ETH for 10 bitcoins, with a 1 ETH 11 | deposit to be forfeit in the case of non-payment: 12 | 13 | ``` 14 | var id = market.offer(30, eth, 10, 1SendBitcoinsHerePlease, 1, eth) 15 | ``` 16 | 17 | This transfers 30 ETH from Alice to escrow in the market. 18 | 19 | Alice can cancel her offer, which will return her 30 ETH and delete 20 | her offer: 21 | 22 | ``` 23 | market.cancel(id) 24 | ``` 25 | 26 | Bob has some bitcoins and wants to buy some ETH. He commits to buy 27 | Alice's offer: 28 | 29 | ``` 30 | market.buy(id) 31 | ``` 32 | 33 | This takes the 1 ETH deposit from Bob into market escrow. Alice can 34 | no longer cancel her offer. Bob now has 1 day to send the required 35 | bitcoin to Alice's address, or he will forfeit his deposit. 36 | 37 | Bob now has a two step verification process to assert that he has 38 | sent his bitcoins. First he needs to tell the market the bitcoin transaction 39 | hash that he used to send the bitcoins: 40 | 41 | ``` 42 | market.confirm(id, bobs_tx_hash) 43 | ``` 44 | 45 | Then he needs to verify this transaction by calling btc-relay: 46 | 47 | ``` 48 | relay.relayTx(rawTx, txIndex, merkleSibling, blockHash, address(market)) 49 | ``` 50 | 51 | Note that the relay needs to know quite a few details of the 52 | transaction, which Bob will have to provide himself. If the transaction 53 | is valid (which currently means having > 6 confirmations), then the 54 | market will be notified and Bob will receive his ETH. 55 | 56 | If Bob does not relay his transaction within 1 day then Alice's 57 | obligation to pay Bob will be voided and she will be able to claim her 58 | funds *and Bob's deposit*. 59 | 60 | ``` 61 | market.claim(id) 62 | market.cancel(id) 63 | ``` 64 | -------------------------------------------------------------------------------- /src/btc_market.sol: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 rain 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | pragma solidity ^0.4.11; 18 | 19 | import 'erc20/erc20.sol'; 20 | import 'btc-tx/btc_tx.sol'; 21 | 22 | // BTC-relay integration 23 | 24 | // Contracts that use btc-relay need to conform to the BitcoinProcessor API. 25 | contract BitcoinProcessor { 26 | // called after successful relayTx call on relay contract 27 | function processTransaction(bytes txBytes, uint256 txHash) returns (int256); 28 | } 29 | 30 | contract EventfulMarket { 31 | event Offer(uint id, uint sell_how_much, ERC20 indexed sell_which_token, uint buy_how_much, bytes20 btc_address); 32 | event Buy(uint id); 33 | event Cancel(uint id); 34 | event Confirm(uint id, uint256 tx_hash); 35 | event Claim(uint id); 36 | event Complete(uint id); 37 | } 38 | 39 | contract BTCMarket is BitcoinProcessor, EventfulMarket { 40 | struct OfferInfo { 41 | uint sell_how_much; 42 | ERC20 sell_which_token; 43 | 44 | uint buy_how_much; 45 | bytes20 btc_address; 46 | 47 | uint deposit_how_much; 48 | ERC20 deposit_which_token; 49 | 50 | address owner; 51 | bool active; 52 | 53 | address buyer; 54 | uint256 confirmed; 55 | uint buy_time; 56 | } 57 | 58 | uint public last_offer_id; 59 | uint public _time_limit; 60 | address public _trusted_relay; 61 | 62 | mapping(uint => OfferInfo) public offers; 63 | mapping(uint256 => uint) public offersByTxHash; 64 | 65 | function () { 66 | throw; 67 | } 68 | 69 | function assert(bool condition) internal { 70 | if (!condition) throw; 71 | } 72 | 73 | function next_id() internal returns (uint) { 74 | last_offer_id++; return last_offer_id; 75 | } 76 | 77 | function getRelay() constant returns (address) { 78 | return _trusted_relay; 79 | } 80 | function getTime() constant returns (uint) { 81 | return block.timestamp; 82 | } 83 | function getTimeLimit() constant returns (uint) { 84 | return _time_limit; 85 | } 86 | 87 | function getOffer(uint id) constant returns (uint, ERC20, uint, bytes20, uint, ERC20) 88 | { 89 | var offer = offers[id]; 90 | return (offer.sell_how_much, offer.sell_which_token, 91 | offer.buy_how_much, offer.btc_address, 92 | offer.deposit_how_much, offer.deposit_which_token); 93 | } 94 | function getBtcAddress(uint id) constant returns (bytes20) { 95 | var offer = offers[id]; 96 | return offer.btc_address; 97 | } 98 | function getOfferByTxHash(uint256 txHash) returns (uint id) { 99 | return offersByTxHash[txHash]; 100 | } 101 | function getBuyer(uint id) constant returns (address) { 102 | var offer = offers[id]; 103 | return offer.buyer; 104 | } 105 | function getOwner(uint id) constant returns (address) { 106 | var offer = offers[id]; 107 | return offer.owner; 108 | } 109 | function getBuyTime(uint id) constant returns (uint) { 110 | var offer = offers[id]; 111 | return offer.buy_time; 112 | } 113 | function isConfirmed(uint id) constant returns (bool) { 114 | var offer = offers[id]; 115 | return (offer.confirmed != 0); 116 | } 117 | function isLocked(uint id) constant returns (bool) { 118 | var offer = offers[id]; 119 | return (offer.buyer != 0x00); 120 | } 121 | function isActive(uint id) constant returns (bool) { 122 | var offer = offers[id]; 123 | return offer.active; 124 | } 125 | function isElapsed(uint id) constant returns (bool) { 126 | return (getTime() - getBuyTime(id) > getTimeLimit()); 127 | } 128 | 129 | 130 | modifier only_locked(uint id) { 131 | assert(isLocked(id)); 132 | _; 133 | } 134 | modifier only_unlocked(uint id) { 135 | assert(!isLocked(id)); 136 | _; 137 | } 138 | modifier only_buyer(uint id) { 139 | assert(msg.sender == getBuyer(id)); 140 | _; 141 | } 142 | modifier only_owner(uint id) { 143 | assert(msg.sender == getOwner(id)); 144 | _; 145 | } 146 | modifier only_relay() { 147 | assert(msg.sender == getRelay()); 148 | _; 149 | } 150 | modifier only_active(uint id) { 151 | assert(isActive(id)); 152 | _; 153 | } 154 | modifier only_elapsed(uint id) { 155 | assert(isElapsed(id)); 156 | _; 157 | } 158 | 159 | function BTCMarket(address BTCRelay, uint time_limit) 160 | { 161 | _trusted_relay = BTCRelay; 162 | _time_limit = time_limit; 163 | } 164 | 165 | function offer( uint sell_how_much, ERC20 sell_which_token, 166 | uint buy_how_much, bytes20 btc_address ) 167 | returns (uint id) 168 | { 169 | return offer(sell_how_much, sell_which_token, 170 | buy_how_much, btc_address, 171 | 0, sell_which_token); 172 | } 173 | function offer( uint sell_how_much, ERC20 sell_which_token, 174 | uint buy_how_much, bytes20 btc_address, 175 | uint deposit_how_much, ERC20 deposit_which_token ) 176 | returns (uint id) 177 | { 178 | assert(sell_how_much > 0); 179 | assert(address(sell_which_token) != 0x0); 180 | assert(buy_how_much > 0); 181 | 182 | assert(sell_which_token.transferFrom(msg.sender, this, sell_how_much)); 183 | 184 | OfferInfo memory info; 185 | info.sell_how_much = sell_how_much; 186 | info.sell_which_token = sell_which_token; 187 | 188 | info.buy_how_much = buy_how_much; 189 | info.btc_address = btc_address; 190 | 191 | info.deposit_how_much = deposit_how_much; 192 | info.deposit_which_token = deposit_which_token; 193 | 194 | info.owner = msg.sender; 195 | info.active = true; 196 | id = next_id(); 197 | offers[id] = info; 198 | 199 | Offer(id, sell_how_much, sell_which_token, buy_how_much, btc_address); 200 | } 201 | function buy (uint id) 202 | only_active(id) 203 | only_unlocked(id) 204 | { 205 | var offer = offers[id]; 206 | offer.buyer = msg.sender; 207 | offer.buy_time = getTime(); 208 | assert(offer.deposit_which_token.transferFrom(msg.sender, this, offer.deposit_how_much)); 209 | 210 | Buy(id); 211 | } 212 | function cancel(uint id) 213 | only_owner(id) 214 | only_active(id) 215 | only_unlocked(id) 216 | { 217 | OfferInfo memory offer = offers[id]; 218 | delete offers[id]; 219 | assert(offer.sell_which_token.transfer(offer.owner, offer.sell_how_much)); 220 | 221 | Cancel(id); 222 | } 223 | function confirm(uint id, uint256 txHash) 224 | only_buyer(id) 225 | { 226 | var offer = offers[id]; 227 | offer.confirmed = txHash; 228 | offersByTxHash[txHash] = id; 229 | 230 | Confirm(id, txHash); 231 | } 232 | function claim(uint id) 233 | only_owner(id) 234 | only_active(id) 235 | only_locked(id) 236 | only_elapsed(id) 237 | { 238 | // send deposit to seller and unlock offer 239 | var offer = offers[id]; 240 | offer.buyer = 0x00; 241 | var refund = offer.deposit_how_much; 242 | offer.deposit_how_much = 0; 243 | assert(offer.deposit_which_token.transfer(offer.owner, refund)); 244 | 245 | Claim(id); 246 | } 247 | function processTransaction(bytes txBytes, uint256 txHash) 248 | only_relay 249 | returns (int256) 250 | { 251 | var id = offersByTxHash[txHash]; 252 | OfferInfo memory offer = offers[id]; 253 | 254 | var sent = BTC.checkValueSent(txBytes, offer.btc_address, offer.buy_how_much); 255 | 256 | if (sent) { 257 | complete(id); 258 | return 0; 259 | } else { 260 | return 1; 261 | } 262 | } 263 | function complete(uint id) 264 | internal 265 | { 266 | OfferInfo memory offer = offers[id]; 267 | delete offers[id]; 268 | assert(offer.sell_which_token.transfer(offer.buyer, offer.sell_how_much)); 269 | assert(offer.deposit_which_token.transfer(offer.buyer, offer.deposit_how_much)); 270 | 271 | Complete(id); 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /src/btc_market_test.sol: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 rain 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | pragma solidity ^0.4.11; 18 | 19 | import 'ds-test/test.sol'; 20 | import 'ds-token/base.sol'; 21 | 22 | import 'btc-tx/btc_tx.sol'; 23 | 24 | import './btc_market.sol'; 25 | 26 | contract MockBTCRelay { 27 | function relayTx(bytes rawTransaction, int256 transactionIndex, 28 | int256[] merkleSibling, int256 blockHash, 29 | int256 contractAddress) 30 | returns (int256) 31 | { 32 | // see testRelayTx for full tx details 33 | bytes memory _txHash = "\x29\xc0\x2a\x5d\x57\x29\x30\xe6\xd3\xde\x6f\xad\x45\xbb\xfd\x8d\x1a\x73\x22\x0f\x86\xf1\xad\xf4\xcd\x1d\xe6\x33\x2c\x33\xac\x3c"; 34 | var txHash = BTC.getBytesLE(_txHash, 0, 32); 35 | var processor = BitcoinProcessor(contractAddress); 36 | return processor.processTransaction(rawTransaction, txHash); 37 | } 38 | } 39 | 40 | contract MarketTester { 41 | address _t; 42 | function _target(address target) { 43 | _t = target; 44 | } 45 | function () { 46 | if(!_t.call(msg.data)) throw; 47 | } 48 | function doApprove(address who, uint how_much, ERC20 token) { 49 | token.approve(who, how_much); 50 | } 51 | } 52 | 53 | contract TestableBTCMarket is BTCMarket { 54 | uint _time; 55 | function TestableBTCMarket(MockBTCRelay relay, uint time_limit) 56 | BTCMarket(relay, time_limit) {} 57 | function getTime() constant returns (uint) { 58 | return _time; 59 | } 60 | function addTime(uint delta) { 61 | _time += delta; 62 | } 63 | } 64 | 65 | contract BTCMarketTest is DSTest { 66 | MarketTester user1; 67 | MarketTester user2; 68 | 69 | TestableBTCMarket otc; 70 | MockBTCRelay relay; 71 | 72 | ERC20 dai; 73 | ERC20 mkr; 74 | 75 | function setUp() { 76 | dai = new DSTokenBase(10 ** 6); 77 | mkr = new DSTokenBase(10 ** 6); 78 | 79 | relay = new MockBTCRelay(); 80 | otc = new TestableBTCMarket(relay, 1 days); 81 | 82 | user1 = new MarketTester(); 83 | user1._target(otc); 84 | user2 = new MarketTester(); 85 | user2._target(otc); 86 | 87 | dai.transfer(user1, 100); 88 | user1.doApprove(otc, 100, dai); 89 | mkr.approve(otc, 30); 90 | } 91 | function testOfferBuyBitcoin() { 92 | bytes20 seller_btc_address = 0x123; 93 | var id = otc.offer(30, mkr, 10, seller_btc_address); 94 | assertEq(id, 1); 95 | assertEq(otc.last_offer_id(), id); 96 | 97 | var (sell_how_much, sell_which_token, 98 | buy_how_much, buy_which_token,,) = otc.getOffer(id); 99 | 100 | assertEq(sell_how_much, 30); 101 | assertEq(sell_which_token, mkr); 102 | 103 | assertEq(buy_how_much, 10); 104 | 105 | assertEq(otc.getBtcAddress(id), seller_btc_address); 106 | } 107 | function testOfferTransferFrom() { 108 | var my_mkr_balance_before = mkr.balanceOf(this); 109 | var id = otc.offer(30, mkr, 10, 0x11); 110 | var my_mkr_balance_after = mkr.balanceOf(this); 111 | 112 | var transferred = my_mkr_balance_before - my_mkr_balance_after; 113 | 114 | assertEq(transferred, 30); 115 | } 116 | function testBuyLocking() { 117 | var id = otc.offer(30, mkr, 10, 0x11); 118 | assert(!otc.isLocked(id)); 119 | BTCMarket(user1).buy(id); 120 | assert(otc.isLocked(id)); 121 | } 122 | function testCancelUnlocked() { 123 | var my_mkr_balance_before = mkr.balanceOf(this); 124 | var id = otc.offer(30, mkr, 10, 0x11); 125 | var my_mkr_balance_after = mkr.balanceOf(this); 126 | otc.cancel(id); 127 | var my_mkr_balance_after_cancel = mkr.balanceOf(this); 128 | 129 | var diff = my_mkr_balance_before - my_mkr_balance_after_cancel; 130 | assertEq(diff, 0); 131 | } 132 | function testFailCancelInactive() { 133 | var id = otc.offer(30, mkr, 10, 0x11); 134 | otc.cancel(id); 135 | otc.cancel(id); 136 | } 137 | function testFailCancelNonOwner() { 138 | var id = otc.offer(30, mkr, 10, 0x11); 139 | BTCMarket(user1).cancel(id); 140 | } 141 | function testFailCancelLocked() { 142 | var id = otc.offer(30, mkr, 10, 0x11); 143 | BTCMarket(user1).buy(id); 144 | otc.cancel(id); 145 | } 146 | function testFailBuyLocked() { 147 | var id = otc.offer(30, mkr, 10, 0x11); 148 | BTCMarket(user1).buy(id); 149 | BTCMarket(user2).buy(id); 150 | } 151 | function testConfirm() { 152 | // after calling `buy` and sending bitcoin, buyer should call 153 | // `confirm` to associate the offer with a bitcoin transaction hash 154 | var id = otc.offer(30, mkr, 10, 0x11); 155 | BTCMarket(user1).buy(id); 156 | 157 | assert(!otc.isConfirmed(id)); 158 | var txHash = 1234; 159 | BTCMarket(user1).confirm(id, txHash); 160 | assert(otc.isConfirmed(id)); 161 | } 162 | function testFailConfirmNonBuyer() { 163 | var id = otc.offer(30, mkr, 10, 0x11); 164 | BTCMarket(user1).buy(id); 165 | BTCMarket(user2).confirm(id, 123); 166 | } 167 | function testGetOfferByTxHash() { 168 | var id = otc.offer(30, mkr, 10, 0x11); 169 | BTCMarket(user1).buy(id); 170 | 171 | var txHash = 1234; 172 | BTCMarket(user1).confirm(id, txHash); 173 | assertEq(otc.getOfferByTxHash(txHash), id); 174 | } 175 | function testLinkedRelay() { 176 | assertEq(otc.getRelay(), relay); 177 | } 178 | function testRelayTxNoMatchingOrder() { 179 | var fail = _relayTx(relay); 180 | // return 1 => unsuccessful check. 181 | assertEq(fail, 1); 182 | } 183 | function testRelayTx() { 184 | // see _relayTx for associated transaction 185 | bytes memory _txHash = "\x29\xc0\x2a\x5d\x57\x29\x30\xe6\xd3\xde\x6f\xad\x45\xbb\xfd\x8d\x1a\x73\x22\x0f\x86\xf1\xad\xf4\xcd\x1d\xe6\x33\x2c\x33\xac\x3c"; 186 | // convert hex txHash to uint 187 | var txHash = BTC.getBytesLE(_txHash, 0, 32); 188 | 189 | var id = otc.offer(30, mkr, 10, hex"8078624453510cd314398e177dcd40dff66d6f9e"); 190 | BTCMarket(user1).buy(id); 191 | BTCMarket(user1).confirm(id, txHash); 192 | 193 | var success = _relayTx(relay); 194 | // return 0 => successful check. 195 | assertEq(success, 0); 196 | } 197 | function testRelayInsufficientTx() { 198 | // see _relayTx for associated transaction 199 | bytes memory _txHash = "\x29\xc0\x2a\x5d\x57\x29\x30\xe6\xd3\xde\x6f\xad\x45\xbb\xfd\x8d\x1a\x73\x22\x0f\x86\xf1\xad\xf4\xcd\x1d\xe6\x33\x2c\x33\xac\x3c"; 200 | // convert hex txHash to uint 201 | var txHash = BTC.getBytesLE(_txHash, 0, 32); 202 | 203 | var id_not_enough = otc.offer(30, mkr, 10, hex"1e6022990700109cb82692bb12085381087d5cea"); 204 | BTCMarket(user1).buy(id_not_enough); 205 | BTCMarket(user1).confirm(id_not_enough, txHash); 206 | 207 | var not_enough = _relayTx(relay); 208 | assertEq(not_enough, 1); 209 | } 210 | function testRelayTxTransfers() { 211 | // see _relayTx for associated transaction 212 | bytes memory _txHash = "\x29\xc0\x2a\x5d\x57\x29\x30\xe6\xd3\xde\x6f\xad\x45\xbb\xfd\x8d\x1a\x73\x22\x0f\x86\xf1\xad\xf4\xcd\x1d\xe6\x33\x2c\x33\xac\x3c"; 213 | // convert hex txHash to uint 214 | var txHash = BTC.getBytesLE(_txHash, 0, 32); 215 | 216 | var my_mkr_balance_before = mkr.balanceOf(this); 217 | var id = otc.offer(30, mkr, 10, hex"8078624453510cd314398e177dcd40dff66d6f9e"); 218 | BTCMarket(user1).buy(id); 219 | BTCMarket(user1).confirm(id, txHash); 220 | 221 | var user1_mkr_balance_before = mkr.balanceOf(user1); 222 | var success = _relayTx(relay); 223 | var user1_mkr_balance_after = mkr.balanceOf(user1); 224 | var my_mkr_balance_after = mkr.balanceOf(this); 225 | 226 | // return 0 => successful check. 227 | assertEq(success, 0); 228 | 229 | var my_balance_diff = my_mkr_balance_before - my_mkr_balance_after; 230 | assertEq(my_balance_diff, 30); 231 | 232 | var balance_diff = user1_mkr_balance_after - user1_mkr_balance_before; 233 | assertEq(balance_diff, 30); 234 | 235 | // offer should be deleted 236 | var (w, x, y, z,,) = otc.getOffer(id); 237 | assertEq(w, 0); 238 | assertEq(y, 0); 239 | } 240 | function testFailRelayTxNonTrusted() { 241 | // see _relayTx for associated transaction 242 | bytes memory _txHash = "\x29\xc0\x2a\x5d\x57\x29\x30\xe6\xd3\xde\x6f\xad\x45\xbb\xfd\x8d\x1a\x73\x22\x0f\x86\xf1\xad\xf4\xcd\x1d\xe6\x33\x2c\x33\xac\x3c"; 243 | // convert hex txHash to uint 244 | var txHash = BTC.getBytesLE(_txHash, 0, 32); 245 | 246 | var id = otc.offer(30, mkr, 10, hex"8078624453510cd314398e177dcd40dff66d6f9e"); 247 | BTCMarket(user1).buy(id); 248 | BTCMarket(user1).confirm(id, txHash); 249 | 250 | var untrusted_relay = new MockBTCRelay(); 251 | // this should fail 252 | var success = _relayTx(untrusted_relay); 253 | } 254 | function _relayTx(MockBTCRelay relay) returns (int256) { 255 | // txid: 29c02a5d572930e6d3de6fad45bbfd8d1a73220f86f1adf4cd1de6332c33ac3c 256 | // txid literal: \x29\xc0\x2a\x5d\x57\x29\x30\xe6\xd3\xde\x6f\xad\x45\xbb\xfd\x8d\x1a\x73\x22\x0f\x86\xf1\xad\xf4\xcd\x1d\xe6\x33\x2c\x33\xac\x3c 257 | // value: 12345678 258 | // value: 11223344 259 | // address: 1CiHhyL4BuD21EJYJBFfUgRKyjPGgB3pVd 260 | // address: 1LG1HY5P53rkUwcwaXAxx1432UDCyfVq9M 261 | // script: OP_DUP OP_HASH160 8078624453510cd314398e177dcd40dff66d6f9e OP_EQUALVERIFY OP_CHECKSIG 262 | // script: OP_DUP OP_HASH160 d340de07e3d72fe70c2d18493e6e3d4c4a3f4ce3 OP_EQUALVERIFY OP_CHECKSIG 263 | // hex: 0100000001a58cbbcbad45625f5ed1f20458f393fe1d1507e254265f09d9746232da4800240000000000ffffffff024e61bc00000000001976a9148078624453510cd314398e177dcd40dff66d6f9e88ac3041ab00000000001976a914d340de07e3d72fe70c2d18493e6e3d4c4a3f4ce388ac00000000 264 | // hex literal: \x01\x00\x00\x00\x01\xa5\x8c\xbb\xcb\xad\x45\x62\x5f\x5e\xd1\xf2\x04\x58\xf3\x93\xfe\x1d\x15\x07\xe2\x54\x26\x5f\x09\xd9\x74\x62\x32\xda\x48\x00\x24\x00\x00\x00\x00\x00\xff\xff\xff\xff\x02\x4e\x61\xbc\x00\x00\x00\x00\x00\x19\x76\xa9\x14\x80\x78\x62\x44\x53\x51\x0c\xd3\x14\x39\x8e\x17\x7d\xcd\x40\xdf\xf6\x6d\x6f\x9e\x88\xac\x30\x41\xab\x00\x00\x00\x00\x00\x19\x76\xa9\x14\xd3\x40\xde\x07\xe3\xd7\x2f\xe7\x0c\x2d\x18\x49\x3e\x6e\x3d\x4c\x4a\x3f\x4c\xe3\x88\xac\x00\x00\x00\x00 265 | 266 | bytes memory mockBytes = "\x01\x00\x00\x00\x01\xa5\x8c\xbb\xcb\xad\x45\x62\x5f\x5e\xd1\xf2\x04\x58\xf3\x93\xfe\x1d\x15\x07\xe2\x54\x26\x5f\x09\xd9\x74\x62\x32\xda\x48\x00\x24\x00\x00\x00\x00\x00\xff\xff\xff\xff\x02\x4e\x61\xbc\x00\x00\x00\x00\x00\x19\x76\xa9\x14\x80\x78\x62\x44\x53\x51\x0c\xd3\x14\x39\x8e\x17\x7d\xcd\x40\xdf\xf6\x6d\x6f\x9e\x88\xac\x30\x41\xab\x00\x00\x00\x00\x00\x19\x76\xa9\x14\xd3\x40\xde\x07\xe3\xd7\x2f\xe7\x0c\x2d\x18\x49\x3e\x6e\x3d\x4c\x4a\x3f\x4c\xe3\x88\xac\x00\x00\x00\x00"; 267 | int256 txIndex = 100; 268 | int256[] memory siblings; 269 | int256 blockHash = 100; 270 | int256 contractAddress = int256(otc); 271 | 272 | return relay.relayTx(mockBytes, txIndex, siblings, blockHash, contractAddress); 273 | } 274 | function _relayInsufficientTx() returns (int256) { 275 | // txid: 75da54d7977ddbc27ac35df9a37e2b93173e854c488e5cdf833e3d848e5860fa 276 | // txid literal: \x75\xda\x54\xd7\x97\x7d\xdb\xc2\x7a\xc3\x5d\xf9\xa3\x7e\x2b\x93\x17\x3e\x85\x4c\x48\x8e\x5c\xdf\x83\x3e\x3d\x84\x8e\x58\x60\xfa 277 | // value: 1 278 | // value: 11223344 279 | // address: 13mcSLqhYjyxK4aeQw7t2fZnngujwrmg3a 280 | // address: 1Hs8vQQ5FBRSFoFD6bvwWAaLUHthc4ZQkz 281 | // script: OP_DUP OP_HASH160 1e6022990700109cb82692bb12085381087d5cea OP_EQUALVERIFY OP_CHECKSIG 282 | // script: OP_DUP OP_HASH160 b8fd6c2b6f03e13de00a0e791a0b23fc1ba0f685 OP_EQUALVERIFY OP_CHECKSIG 283 | // hex: 0100000001a58cbbcbad45625f5ed1f20458f393fe1d1507e254265f09d9746232da4800240000000000ffffffff0201000000000000001976a9141e6022990700109cb82692bb12085381087d5cea88ac3041ab00000000001976a914b8fd6c2b6f03e13de00a0e791a0b23fc1ba0f68588ac00000000 284 | // hex literal: \x01\x00\x00\x00\x01\xa5\x8c\xbb\xcb\xad\x45\x62\x5f\x5e\xd1\xf2\x04\x58\xf3\x93\xfe\x1d\x15\x07\xe2\x54\x26\x5f\x09\xd9\x74\x62\x32\xda\x48\x00\x24\x00\x00\x00\x00\x00\xff\xff\xff\xff\x02\x01\x00\x00\x00\x00\x00\x00\x00\x19\x76\xa9\x14\x1e\x60\x22\x99\x07\x00\x10\x9c\xb8\x26\x92\xbb\x12\x08\x53\x81\x08\x7d\x5c\xea\x88\xac\x30\x41\xab\x00\x00\x00\x00\x00\x19\x76\xa9\x14\xb8\xfd\x6c\x2b\x6f\x03\xe1\x3d\xe0\x0a\x0e\x79\x1a\x0b\x23\xfc\x1b\xa0\xf6\x85\x88\xac\x00\x00\x00\x00 285 | 286 | bytes memory mockBytes = "\x01\x00\x00\x00\x01\xa5\x8c\xbb\xcb\xad\x45\x62\x5f\x5e\xd1\xf2\x04\x58\xf3\x93\xfe\x1d\x15\x07\xe2\x54\x26\x5f\x09\xd9\x74\x62\x32\xda\x48\x00\x24\x00\x00\x00\x00\x00\xff\xff\xff\xff\x02\x01\x00\x00\x00\x00\x00\x00\x00\x19\x76\xa9\x14\x1e\x60\x22\x99\x07\x00\x10\x9c\xb8\x26\x92\xbb\x12\x08\x53\x81\x08\x7d\x5c\xea\x88\xac\x30\x41\xab\x00\x00\x00\x00\x00\x19\x76\xa9\x14\xb8\xfd\x6c\x2b\x6f\x03\xe1\x3d\xe0\x0a\x0e\x79\x1a\x0b\x23\xfc\x1b\xa0\xf6\x85\x88\xac\x00\x00\x00\x00"; 287 | int256 txIndex = 100; 288 | int256[] memory siblings; 289 | int256 blockHash = 100; 290 | int256 contractAddress = int256(otc); 291 | 292 | return relay.relayTx(mockBytes, txIndex, siblings, blockHash, contractAddress); 293 | } 294 | function testDeposit() { 295 | // create an offer requiring a 5 DAI deposit 296 | var id = otc.offer(30, mkr, 10, hex"8078624453510cd314398e177dcd40dff66d6f9e", 5, dai); 297 | 298 | // check deposit is taken when user places order 299 | dai.transfer(user1, 100); 300 | user1.doApprove(otc, 100, dai); 301 | var user_dai_balance_before_buy = dai.balanceOf(user1); 302 | BTCMarket(user1).buy(id); 303 | var user_dai_balance_after_buy = dai.balanceOf(user1); 304 | 305 | var user_buy_diff = user_dai_balance_before_buy - user_dai_balance_after_buy; 306 | assertEq(user_buy_diff, 5); 307 | 308 | bytes memory _txHash = "\x29\xc0\x2a\x5d\x57\x29\x30\xe6\xd3\xde\x6f\xad\x45\xbb\xfd\x8d\x1a\x73\x22\x0f\x86\xf1\xad\xf4\xcd\x1d\xe6\x33\x2c\x33\xac\x3c"; 309 | var txHash = BTC.getBytesLE(_txHash, 0, 32); 310 | 311 | BTCMarket(user1).confirm(id, txHash); 312 | 313 | // check deposit refunded when user relays good tx */ 314 | var success = _relayTx(relay); 315 | assertEq(success, 0); 316 | var user_dai_balance_after_relay = dai.balanceOf(user1); 317 | assertEq(user_dai_balance_after_relay, user_dai_balance_before_buy); 318 | } 319 | function testClaim() { 320 | // create an offer requiring a 5 DAI deposit 321 | var id = otc.offer(30, mkr, 10, hex"8078624453510cd314398e177dcd40dff66d6f9e", 5, dai); 322 | 323 | dai.transfer(user1, 5); 324 | user1.doApprove(otc, 5, dai); 325 | BTCMarket(user1).buy(id); 326 | 327 | otc.addTime(2 days); 328 | 329 | var dai_before = dai.balanceOf(this); 330 | otc.claim(id); 331 | assertEq(dai.balanceOf(this) - dai_before, 5); 332 | } 333 | function testCancelAfterClaim() { 334 | // create an offer requiring a 5 DAI deposit 335 | var id = otc.offer(30, mkr, 10, hex"8078624453510cd314398e177dcd40dff66d6f9e", 5, dai); 336 | 337 | dai.transfer(user1, 5); 338 | user1.doApprove(otc, 5, dai); 339 | BTCMarket(user1).buy(id); 340 | 341 | otc.addTime(2 days); 342 | 343 | otc.claim(id); 344 | 345 | var mkr_before = mkr.balanceOf(this); 346 | otc.cancel(id); 347 | assertEq(mkr.balanceOf(this) - mkr_before, 30); 348 | } 349 | function testFailClaimBeforeElapsed() { 350 | // create an offer requiring a 5 DAI deposit 351 | var id = otc.offer(30, mkr, 10, hex"8078624453510cd314398e177dcd40dff66d6f9e", 5, dai); 352 | 353 | dai.transfer(user1, 5); 354 | user1.doApprove(otc, 5, dai); 355 | BTCMarket(user1).buy(id); 356 | 357 | otc.claim(id); 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /todo.md: -------------------------------------------------------------------------------- 1 | We want to integrate btc relay to Maker Market. 2 | 3 | There are many ways this could be imlemented and developed. For now 4 | we will try and generate a proof of concept, which can then be made 5 | into a minimum viable product. 6 | 7 | Constraints: 8 | 9 | 1. separate order book from SimpleMarket 10 | 11 | We'll start by making a PoC in the backend. This will require 12 | creating a new contract in which BTC/TOKEN offers can be made, where 13 | TOKEN is any of the tokens that SimpleMarket can accept. 14 | 15 | 16 | Backend workflow: 17 | 18 | Selling token for BTC: 19 | 20 | 21 | Seller calls .offer(100, 'MKR', 5, 'BTC', BTCADDRESS) 22 | 23 | anything but 'BTC' should fail (for now) 24 | 25 | BTCADDRESS is a seller controlled address at which they expect 26 | payment. 27 | 28 | assert BTCADDRESS is valid? 29 | 30 | entry into orderbook with orderID 31 | 32 | 100 MKR transferFrom seller 33 | 34 | 35 | Buyer calls .buy(orderid) 36 | 37 | Offer is LOCKED to buyer address 38 | 39 | Contract awaits confirmation of btc tx to BTCADDRESS 40 | 41 | On confirmation, funds transferred to buyer. 42 | 43 | Needs anti abuse consideration, or someone can lock all orders. 44 | 45 | 46 | Offer has a bool `locked` attribute. 47 | 48 | When an Offer is Locked, it cannot be canceled by the creator. 49 | Only btc payment verification or a timeout can unlock an offer. 50 | 51 | 52 | 53 | Selling BTC for token: 54 | 55 | Flow here is different to above. We can't transferFrom BTC from the 56 | seller because we have no way to interact with the Bitcoin chain. 57 | 58 | We need to make a provisional offer, that only has token transfer 59 | when it is filled and btc transaction is verified. 60 | 61 | 62 | Seller calls .offer(5, 'BTC', 100, 'MKR') 63 | 64 | entry into orderbook 65 | 66 | 67 | Buyer calls .buy(orderid, BTCADDRESS) 68 | 69 | transferFrom buyer 70 | 71 | Offer is LOCKED 72 | 73 | Contract awaits confirmation of btc tx to BTCADDRESS 74 | 75 | 76 | Problem here: the seller needs to be paying attention to the 77 | orderbook, or they'll miss the transaction window. This isn't 78 | great. 79 | 80 | Really, they should be automating this, with a bot that watches the 81 | orderbook that can effect the btc transfer. 82 | --------------------------------------------------------------------------------