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  }