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  }