decred.org/dcrdex@v1.0.5/dex/networks/eth/contracts/ETHSwapV0.sol (about) 1 // SPDX-License-Identifier: BlueOak-1.0.0 2 // pragma should be as specific as possible to allow easier validation. 3 pragma solidity = 0.8.18; 4 5 // ETHSwap creates a contract to be deployed on an ethereum network. After 6 // deployed, it keeps a map of swaps that facilitates atomic swapping of 7 // ethereum with other crypto currencies that support time locks. 8 // 9 // It accomplishes this by holding funds sent to this contract until certain 10 // conditions are met. An initiator sends an amount of funds along with byte 11 // code that tells the contract to insert a swap struct into the public map. At 12 // this point the funds belong to the contract, and cannot be accessed by 13 // anyone else, not even the contract's deployer. The initiator sets a 14 // participant, a secret hash, and a refund blocktime. The participant can 15 // redeem at any time after the initiation transaction is mined if they have 16 // the secret that hashes to the secret hash. Otherwise, anyone can refund 17 // funds any time after the locktime. 18 // 19 // This contract has no limits on gas used for any transactions. 20 // 21 // This contract cannot be used by other contracts or by a third party mediating 22 // the swap or multisig wallets. 23 // 24 // This code should be verifiable as resulting in a certain on-chain contract 25 // by compiling with the correct version of solidity and comparing the 26 // resulting byte code to the data in the original transaction. 27 contract ETHSwap { 28 // State is a type that hold's a contract's state. Empty is the uninitiated 29 // or null value. 30 enum State { Empty, Filled, Redeemed, Refunded } 31 32 // Swap holds information related to one side of a single swap. The order of 33 // the struct fields is important to efficiently pack the struct into as few 34 // 256-bit slots as possible to reduce gas cost. In particular, the 160-bit 35 // address can pack with the 8-bit State. 36 struct Swap { 37 bytes32 secret; 38 uint256 value; 39 uint initBlockNumber; 40 uint refundBlockTimestamp; 41 address initiator; 42 address participant; 43 State state; 44 } 45 46 // swaps is a map of swap secret hashes to swaps. It can be read by anyone 47 // for free. 48 mapping(bytes32 => Swap) public swaps; 49 50 // constructor is empty. This contract has no connection to the original 51 // sender after deployed. It can only be interacted with by users 52 // initiating, redeeming, and refunding swaps. 53 constructor() {} 54 55 // isRefundable checks that a swap can be refunded. The requirements are 56 // the state is Filled, and the block timestamp be after the swap's stored 57 // refundBlockTimestamp. 58 function isRefundable(bytes32 secretHash) public view returns (bool) { 59 Swap storage swapToCheck = swaps[secretHash]; 60 return swapToCheck.state == State.Filled && 61 block.timestamp >= swapToCheck.refundBlockTimestamp; 62 } 63 64 // senderIsOrigin ensures that this contract cannot be used by other 65 // contracts, which reduces possible attack vectors. 66 modifier senderIsOrigin() { 67 require(tx.origin == msg.sender, "sender != origin"); 68 _; 69 } 70 71 // swap returns a single swap from the swaps map. 72 function swap(bytes32 secretHash) 73 public view returns(Swap memory) 74 { 75 return swaps[secretHash]; 76 } 77 78 struct Initiation { 79 uint refundTimestamp; 80 bytes32 secretHash; 81 address participant; 82 uint value; 83 } 84 85 // initiate initiates an array of swaps. It checks that all of the 86 // swaps have a non zero redemptionTimestamp and value, and that none of 87 // the secret hashes have ever been used previously. The function also makes 88 // sure that msg.value is equal to the sum of the values of all the swaps. 89 // Once initiated, each swap's state is set to Filled. The msg.value is now 90 // in the custody of the contract and can only be retrieved through redeem 91 // or refund. 92 function initiate(Initiation[] calldata initiations) 93 public 94 payable 95 senderIsOrigin() 96 { 97 uint initVal = 0; 98 for (uint i = 0; i < initiations.length; i++) { 99 Initiation calldata initiation = initiations[i]; 100 Swap storage swapToUpdate = swaps[initiation.secretHash]; 101 102 require(initiation.value > 0, "0 val"); 103 require(initiation.refundTimestamp > 0, "0 refundTimestamp"); 104 require(swapToUpdate.state == State.Empty, "dup swap"); 105 106 swapToUpdate.initBlockNumber = block.number; 107 swapToUpdate.refundBlockTimestamp = initiation.refundTimestamp; 108 swapToUpdate.initiator = msg.sender; 109 swapToUpdate.participant = initiation.participant; 110 swapToUpdate.value = initiation.value; 111 swapToUpdate.state = State.Filled; 112 113 initVal += initiation.value; 114 } 115 116 require(initVal == msg.value, "bad val"); 117 } 118 119 struct Redemption { 120 bytes32 secret; 121 bytes32 secretHash; 122 } 123 124 // redeem redeems a contract. It checks that the sender is not a contract, 125 // and that the secret hash hashes to secretHash. msg.value is tranfered 126 // from the contract to the sender. 127 // 128 // It is important to note that this uses call.value which comes with no 129 // restrictions on gas used. This has the potential to open the contract up 130 // to a reentry attack. A reentry attack inserts extra code in call.value 131 // that executes before the function returns. This is why it is very 132 // important to check the state of the contract first, and change the state 133 // before proceeding to send. That way, the nested attacking function will 134 // throw upon trying to call redeem a second time. Currently, reentry is also 135 // not possible because contracts cannot use this contract. 136 function redeem(Redemption[] calldata redemptions) 137 public 138 senderIsOrigin() 139 { 140 uint amountToRedeem = 0; 141 for (uint i = 0; i < redemptions.length; i++) { 142 Redemption calldata redemption = redemptions[i]; 143 Swap storage swapToRedeem = swaps[redemption.secretHash]; 144 145 require(swapToRedeem.state == State.Filled, "bad state"); 146 require(swapToRedeem.participant == msg.sender, "bad participant"); 147 require(sha256(abi.encodePacked(redemption.secret)) == redemption.secretHash, 148 "bad secret"); 149 150 swapToRedeem.state = State.Redeemed; 151 swapToRedeem.secret = redemption.secret; 152 amountToRedeem += swapToRedeem.value; 153 } 154 155 (bool ok, ) = payable(msg.sender).call{value: amountToRedeem}(""); 156 require(ok == true, "transfer failed"); 157 } 158 159 // refund refunds a contract. It checks that the sender is not a contract, 160 // and that the refund time has passed. msg.value is transferred from the 161 // contract to the initiator. 162 // 163 // It is important to note that this also uses call.value which comes with no 164 // restrictions on gas used. See redeem for more info. 165 function refund(bytes32 secretHash) 166 public 167 senderIsOrigin() 168 { 169 require(isRefundable(secretHash), "not refundable"); 170 Swap storage swapToRefund = swaps[secretHash]; 171 swapToRefund.state = State.Refunded; 172 (bool ok, ) = payable(swapToRefund.initiator).call{value: swapToRefund.value}(""); 173 require(ok == true, "transfer failed"); 174 } 175 }