decred.org/dcrdex@v1.0.5/dex/networks/erc20/contracts/ERC20SwapV0.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. In 6 // order to save on gas fees, a separate ERC20Swap contract is deployed 7 // for each ERC20 token. After deployed, it keeps a map of swaps that 8 // facilitates atomic swapping of ERC20 tokens with other crypto currencies 9 // that support time locks. 10 // 11 // It accomplishes this by holding tokens acquired during a swap initiation 12 // until conditions are met. Prior to initiating a swap, the initiator must 13 // approve the ERC20Swap contract to be able to spend the initiator's tokens. 14 // When calling initiate, the necessary tokens for swaps are transferred to 15 // the swap contract. At this point the funds belong to the contract, and 16 // cannot be accessed by anyone else, not even the contract's deployer. The 17 // initiator sets a secret hash, a blocktime the funds will be accessible should 18 // they not be redeemed, and a participant who can redeem before or after the 19 // locktime. The participant can redeem at any time after the initiation 20 // transaction is mined if they have the secret that hashes to the secret hash. 21 // Otherwise, the initiator can refund funds any time after the locktime. 22 // 23 // This contract has no limits on gas used for any transactions. 24 // 25 // This contract cannot be used by other contracts or by a third party mediating 26 // the swap or multisig wallets. 27 contract ERC20Swap { 28 bytes4 private constant TRANSFER_FROM_SELECTOR = bytes4(keccak256("transferFrom(address,address,uint256)")); 29 bytes4 private constant TRANSFER_SELECTOR = bytes4(keccak256("transfer(address,uint256)")); 30 31 address public immutable token_address; 32 33 // State is a type that hold's a contract's state. Empty is the uninitiated 34 // or null value. 35 enum State { Empty, Filled, Redeemed, Refunded } 36 37 // Swap holds information related to one side of a single swap. The order of 38 // the struct fields is important to efficiently pack the struct into as few 39 // 256-bit slots as possible to reduce gas cost. In particular, the 160-bit 40 // address can pack with the 8-bit State. 41 struct Swap { 42 bytes32 secret; 43 uint256 value; 44 uint initBlockNumber; 45 uint refundBlockTimestamp; 46 address initiator; 47 address participant; 48 State state; 49 } 50 51 // swaps is a map of swap secret hashes to swaps. It can be read by anyone 52 // for free. 53 mapping(bytes32 => Swap) public swaps; 54 55 constructor(address token) { 56 token_address = token; 57 } 58 59 // senderIsOrigin ensures that this contract cannot be used by other 60 // contracts, which reduces possible attack vectors. 61 modifier senderIsOrigin() { 62 require(tx.origin == msg.sender, "sender != origin"); 63 _; 64 } 65 66 // swap returns a single swap from the swaps map. 67 function swap(bytes32 secretHash) 68 public view returns(Swap memory) 69 { 70 return swaps[secretHash]; 71 } 72 73 // Initiation is used to specify the information needed to initiate a swap. 74 struct Initiation { 75 uint refundTimestamp; 76 bytes32 secretHash; 77 address participant; 78 uint value; 79 } 80 81 // initiate initiates an array of swaps. It checks that all of the swaps 82 // have a non zero redemptionTimestamp and value, and that none of the 83 // secret hashes have ever been used previously. Once initiated, each 84 // swap's state is set to Filled. The tokens equal to the sum of each 85 // swap's value are now in the custody of the contract and can only be 86 // retrieved through redeem or refund. 87 function initiate(Initiation[] calldata initiations) 88 public 89 senderIsOrigin() 90 { 91 uint initVal = 0; 92 for (uint i = 0; i < initiations.length; i++) { 93 Initiation calldata initiation = initiations[i]; 94 Swap storage swapToUpdate = swaps[initiation.secretHash]; 95 96 require(initiation.value > 0, "0 val"); 97 require(initiation.refundTimestamp > 0, "0 refundTimestamp"); 98 require(swapToUpdate.state == State.Empty, "dup secret hash"); 99 100 swapToUpdate.initBlockNumber = block.number; 101 swapToUpdate.refundBlockTimestamp = initiation.refundTimestamp; 102 swapToUpdate.initiator = msg.sender; 103 swapToUpdate.participant = initiation.participant; 104 swapToUpdate.value = initiation.value; 105 swapToUpdate.state = State.Filled; 106 107 initVal += initiation.value; 108 } 109 110 bool success; 111 bytes memory data; 112 (success, data) = token_address.call(abi.encodeWithSelector(TRANSFER_FROM_SELECTOR, msg.sender, address(this), initVal)); 113 require(success && (data.length == 0 || abi.decode(data, (bool))), 'transfer from failed'); 114 } 115 116 // Redemption is used to specify the information needed to redeem a swap. 117 struct Redemption { 118 bytes32 secret; 119 bytes32 secretHash; 120 } 121 122 // redeem redeems an array of swaps contract. It checks that the sender is 123 // not a contract, and that the secret hash hashes to secretHash. The ERC20 124 // tokens are transferred from the contract to the sender. 125 function redeem(Redemption[] calldata redemptions) 126 public 127 senderIsOrigin() 128 { 129 uint amountToRedeem = 0; 130 for (uint i = 0; i < redemptions.length; i++) { 131 Redemption calldata redemption = redemptions[i]; 132 Swap storage swapToRedeem = swaps[redemption.secretHash]; 133 134 require(swapToRedeem.state == State.Filled, "bad state"); 135 require(swapToRedeem.participant == msg.sender, "bad participant"); 136 require(sha256(abi.encodePacked(redemption.secret)) == redemption.secretHash, 137 "bad secret"); 138 139 swapToRedeem.state = State.Redeemed; 140 swapToRedeem.secret = redemption.secret; 141 amountToRedeem += swapToRedeem.value; 142 } 143 144 bool success; 145 bytes memory data; 146 (success, data) = token_address.call(abi.encodeWithSelector(TRANSFER_SELECTOR, msg.sender, amountToRedeem)); 147 require(success && (data.length == 0 || abi.decode(data, (bool))), 'transfer failed'); 148 } 149 150 151 // isRefundable checks that a swap can be refunded. The requirements are 152 // the initiator is msg.sender, the state is Filled, and the block 153 // timestamp be after the swap's stored refundBlockTimestamp. 154 function isRefundable(bytes32 secretHash) public view returns (bool) { 155 Swap storage swapToCheck = swaps[secretHash]; 156 return swapToCheck.state == State.Filled && 157 swapToCheck.initiator == msg.sender && 158 block.timestamp >= swapToCheck.refundBlockTimestamp; 159 } 160 161 // refund refunds a contract. It checks that the sender is not a contract, 162 // and that the refund time has passed. An amount of ERC20 tokens equal to 163 // swap.value is transferred from the contract to the sender. 164 function refund(bytes32 secretHash) 165 public 166 senderIsOrigin() 167 { 168 require(isRefundable(secretHash), "not refundable"); 169 Swap storage swapToRefund = swaps[secretHash]; 170 swapToRefund.state = State.Refunded; 171 172 bool success; 173 bytes memory data; 174 (success, data) = token_address.call(abi.encodeWithSelector(TRANSFER_SELECTOR, msg.sender, swapToRefund.value)); 175 require(success && (data.length == 0 || abi.decode(data, (bool))), 'transfer failed'); 176 } 177 }