Skip to content

eth: Solidity atomic swap contract discussion. #1001

@JoeGruffins

Description

@JoeGruffins

Ethereum

Ethereum does not have utxo or opcodes. It uses accounts and balances. Further more, there are "normal" accounts and contracts, which also count as accounts. Contract accounts can be sent transactions with special data that include instructions. We can use these instructions to make atomic swaps.

UTXO <-> ETH Contract

This contract is a stripped down version of this.

All of the individual functions have been checked to work as intended.

full contract
// SPDX-License-Identifier: BlueOak-1.0.0
pragma solidity >=0.7.0 <0.9.0;
contract DecredSwaps {
    enum State { Empty, Filled, Redeemed, Refunded }

    struct Swap {
        uint initBlockNumber;
        uint refundBlockTimestamp;
        bytes32 secretHash;
        bytes32 secret;
        address initiator;
        address participant;
        uint256 value;
        State state;
    }

    mapping(bytes32 => Swap) public swaps;

    constructor() {}

    modifier isRefundable(bytes32 secretHash, address refunder) {
        require(swaps[secretHash].state == State.Filled);
        require(swaps[secretHash].initiator == refunder);
        uint refundBlockTimestamp = swaps[secretHash].refundBlockTimestamp;
        require(block.timestamp >= refundBlockTimestamp);
        _;
    }

    modifier isRedeemable(bytes32 secretHash, bytes32 secret, address redeemer) {
        require(swaps[secretHash].state == State.Filled);
        require(swaps[secretHash].participant == redeemer);
        require(sha256(abi.encodePacked(secret)) == secretHash);
        _;
    }

    modifier isNotInitiated(bytes32 secretHash) {
        require(swaps[secretHash].state == State.Empty);
        _;
    }

    modifier hasNoNilValues(uint refundTime) {
        require(msg.value > 0);
        require(refundTime > 0);
        _;
    }

    function initiate(uint refundTimestamp, bytes32 secretHash, address participant)
        public
        payable
        hasNoNilValues(refundTimestamp)
        isNotInitiated(secretHash)
    {
        swaps[secretHash].initBlockNumber = block.number;
        swaps[secretHash].refundBlockTimestamp = refundTimestamp;
        swaps[secretHash].secretHash = secretHash;
        swaps[secretHash].initiator = msg.sender;
        swaps[secretHash].participant = participant;
        swaps[secretHash].value = msg.value;
        swaps[secretHash].state = State.Filled;
    }

    function redeem(bytes32 secret, bytes32 secretHash)
        public
        isRedeemable(secretHash, secret, msg.sender)
    {
        payable(msg.sender).transfer(swaps[secretHash].value);
        swaps[secretHash].state = State.Redeemed;
        swaps[secretHash].secret = secret;
    }

    function refund(bytes32 secretHash)
        public
        isRefundable(secretHash, msg.sender)
    {
        payable(msg.sender).transfer(swaps[secretHash].value);
        swaps[secretHash].state = State.Refunded;
    }
}

It works by keeping a map of all swaps. Apparently, the size of the map will not influence the cost of using the contract, and we can grow it forever. The map is keyed by the swap contract's secret's hash. It has a state that increases depending upon where along the swap we are. Any unitiated swap is 0/Empty. Anyone can then initiate the swap with a secret hash moving the status to 1/Filled. Initiating also requires a value, locktime, and participant (I'm not sure if the participant has been checked as a valid hex at this point). The funds are now only spendable when either the participant supplies the secret that hashes to the secret hash, or the refundBlockstamp has passed and the owner of the transaction is the initiator. The status is moved to 2/Redeemed or 3/Refunded. If redeemed, the secret has been saved by the blockchain and can be viewed publicly on the blockchain, allowing the other side of the swap to redeem their funds.

This contract will need to first be deployed on whichever network before it can be used which entails sending a transaction(s?) that creates it.

Considerations

  1. The contract cannot be used without eth!
    An eth contract cannot be interacted with without already having eth. For this contract, that means initiating, refunding, and redeeming. This could cause major problems if not checked properly. A party on either side of the trade must have some eth reserves to trade and keep them throughout the duration of the trade or risk loosing the entire trade. The official fix comming soon™.

  2. The original has notifications.
    Do we want to use these? They have been cut out of this simplified version.

  3. The original also saves the initial block time.
    It seems like we are interested in the block confirmations mostly for the initiation, so it has been switch to save the blocknumber at that time. We could, probably, track the transaction that pays the initiation, but I think we would not be using the smart contracted as intended then. It would also introduce more complexity and possible bugs.

Unknowns

  1. Security.
    I am hoping that after we have a contract that works for us, but before enabling mainnet, we can have some ethereum experts browse our code. I will probe around for some "experts" towards this end. We have all heard stories about loopholes in poorly written contracts that result in user's lost funds.
  2. UTXO, ETH <-> ERC token
    I have not researched this yet and this contract does not enable the ERC side of this.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions