Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

eth: Solidity atomic swap contract discussion. #1001

Closed
JoeGruffins opened this issue Mar 5, 2021 · 23 comments
Closed

eth: Solidity atomic swap contract discussion. #1001

JoeGruffins opened this issue Mar 5, 2021 · 23 comments

Comments

@JoeGruffins
Copy link
Member

JoeGruffins commented Mar 5, 2021

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.
@buck54321
Copy link
Member

We must be able to audit the swap, including refundBlockTimestamp, value, participant, etc. It's not clear to me that we can do that here. Since mapping(bytes32 => Swap) public swaps; is public, does that mean that we can somehow inspect the struct Swap by selecting it with its secret hash?

@JoeGruffins
Copy link
Member Author

JoeGruffins commented Mar 10, 2021

I am able to do so using truffle but not %100 sure that's not due to some helper method. I think that all the data stored in that map is public.

@buck54321
Copy link
Member

It would be informative to compile the solidity contract and use abigen to generate the Go bindings so that we can examine the exported methods and fields.

@JoeGruffins
Copy link
Member Author

JoeGruffins commented Mar 11, 2021

$ ./abigen --sol DecredSwaps.sol --pkg main --out decredswaps.go

$ cat decredswaps.go
// Code generated - DO NOT EDIT.
// This file is a generated binding and any manual changes will be lost.

package main

import (
	"math/big"
	"strings"

	ethereum "github.com/ethereum/go-ethereum"
	"github.com/ethereum/go-ethereum/accounts/abi"
	"github.com/ethereum/go-ethereum/accounts/abi/bind"
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/core/types"
	"github.com/ethereum/go-ethereum/event"
)

// Reference imports to suppress errors if they are not otherwise used.
var (
	_ = big.NewInt
	_ = strings.NewReader
	_ = ethereum.NotFound
	_ = bind.Bind
	_ = common.Big1
	_ = types.BloomLookup
	_ = event.NewSubscription
)

// DecredSwapsABI is the input ABI used to generate the binding from.
const DecredSwapsABI = "[{\"inputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"refundTimestamp\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"secretHash\",\"type\":\"bytes32\"},{\"internalType\":\"address\",\"name\":\"participant\",\"type\":\"address\"}],\"name\":\"initiate\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"secret\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"secretHash\",\"type\":\"bytes32\"}],\"name\":\"redeem\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"secretHash\",\"type\":\"bytes32\"}],\"name\":\"refund\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"name\":\"swaps\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"initBlockNumber\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"refundBlockTimestamp\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"secretHash\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"secret\",\"type\":\"bytes32\"},{\"internalType\":\"address\",\"name\":\"initiator\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"participant\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"},{\"internalType\":\"enumDecredSwaps.State\",\"name\":\"state\",\"type\":\"uint8\"}],\"stateMutability\":\"view\",\"type\":\"function\"}]"

// DecredSwapsFuncSigs maps the 4-byte function signature to its string representation.
var DecredSwapsFuncSigs = map[string]string{
	"ae052147": "initiate(uint256,bytes32,address)",
	"b31597ad": "redeem(bytes32,bytes32)",
	"7249fbb6": "refund(bytes32)",
	"eb84e7f2": "swaps(bytes32)",
}

// DecredSwapsBin is the compiled bytecode used for deploying new contracts.
var DecredSwapsBin = "0x608060405234801561001057600080fd5b50610599806100206000396000f3fe60806040526004361061003f5760003560e01c80637249fbb614610044578063ae05214714610066578063b31597ad14610079578063eb84e7f214610099575b600080fd5b34801561005057600080fd5b5061006461005f366004610421565b6100d6565b005b610064610074366004610472565b6101bf565b34801561008557600080fd5b50610064610094366004610451565b610286565b3480156100a557600080fd5b506100b96100b4366004610421565b6103ce565b6040516100cd9897969594939291906104f7565b60405180910390f35b8033600160008381526020819052604090206007015460ff16600381111561010e57634e487b7160e01b600052602160045260246000fd5b1461011857600080fd5b6000828152602081905260409020600401546001600160a01b0382811691161461014157600080fd5b6000828152602081905260409020600101544281111561016057600080fd5b600084815260208190526040808220600601549051339282156108fc02929190818181858888f1935050505015801561019d573d6000803e3d6000fd5b505050600091825250602081905260409020600701805460ff19166003179055565b82600034116101cd57600080fd5b600081116101da57600080fd5b826000808281526020819052604090206007015460ff16600381111561021057634e487b7160e01b600052602160045260246000fd5b1461021a57600080fd5b6000848152602081905260409020438155600180820187905560028201869055600482018054336001600160a01b0319918216179091556005830180549091166001600160a01b0387161790553460068301556007909101805460ff1916828002179055505050505050565b808233600160008481526020819052604090206007015460ff1660038111156102bf57634e487b7160e01b600052602160045260246000fd5b146102c957600080fd5b6000838152602081905260409020600501546001600160a01b038281169116146102f257600080fd5b8260028360405160200161030691906104b5565b60408051601f1981840301815290829052610320916104be565b602060405180830381855afa15801561033d573d6000803e3d6000fd5b5050506040513d601f19601f820116820180604052508101906103609190610439565b1461036a57600080fd5b600084815260208190526040808220600601549051339282156108fc02929190818181858888f193505050501580156103a7573d6000803e3d6000fd5b50505060009182525060208190526040902060078101805460ff1916600217905560030155565b6000602081905290815260409020805460018201546002830154600384015460048501546005860154600687015460079097015495969495939492936001600160a01b0392831693919092169160ff1688565b600060208284031215610432578081fd5b5035919050565b60006020828403121561044a578081fd5b5051919050565b60008060408385031215610463578081fd5b50508035926020909101359150565b600080600060608486031215610486578081fd5b833592506020840135915060408401356001600160a01b03811681146104aa578182fd5b809150509250925092565b90815260200190565b60008251815b818110156104de57602081860181015185830152016104c4565b818111156104ec5782828501525b509190910192915050565b8881526020810188905260408101879052606081018690526001600160a01b038581166080830152841660a082015260c0810183905261010081016004831061055057634e487b7160e01b600052602160045260246000fd5b8260e0830152999850505050505050505056fea2646970667358221220ad691af3e42d7584399b943644c249d9264c53c10d8c0405f5330bc5152686da64736f6c63430008010033"

// DeployDecredSwaps deploys a new Ethereum contract, binding an instance of DecredSwaps to it.
func DeployDecredSwaps(auth *bind.TransactOpts, backend bind.ContractBackend) (common.Address, *types.Transaction, *DecredSwaps, error) {
	parsed, err := abi.JSON(strings.NewReader(DecredSwapsABI))
	if err != nil {
		return common.Address{}, nil, nil, err
	}

	address, tx, contract, err := bind.DeployContract(auth, parsed, common.FromHex(DecredSwapsBin), backend)
	if err != nil {
		return common.Address{}, nil, nil, err
	}
	return address, tx, &DecredSwaps{DecredSwapsCaller: DecredSwapsCaller{contract: contract}, DecredSwapsTransactor: DecredSwapsTransactor{contract: contract}, DecredSwapsFilterer: DecredSwapsFilterer{contract: contract}}, nil
}

// DecredSwaps is an auto generated Go binding around an Ethereum contract.
type DecredSwaps struct {
	DecredSwapsCaller     // Read-only binding to the contract
	DecredSwapsTransactor // Write-only binding to the contract
	DecredSwapsFilterer   // Log filterer for contract events
}

// DecredSwapsCaller is an auto generated read-only Go binding around an Ethereum contract.
type DecredSwapsCaller struct {
	contract *bind.BoundContract // Generic contract wrapper for the low level calls
}

// DecredSwapsTransactor is an auto generated write-only Go binding around an Ethereum contract.
type DecredSwapsTransactor struct {
	contract *bind.BoundContract // Generic contract wrapper for the low level calls
}

// DecredSwapsFilterer is an auto generated log filtering Go binding around an Ethereum contract events.
type DecredSwapsFilterer struct {
	contract *bind.BoundContract // Generic contract wrapper for the low level calls
}

// DecredSwapsSession is an auto generated Go binding around an Ethereum contract,
// with pre-set call and transact options.
type DecredSwapsSession struct {
	Contract     *DecredSwaps      // Generic contract binding to set the session for
	CallOpts     bind.CallOpts     // Call options to use throughout this session
	TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session
}

// DecredSwapsCallerSession is an auto generated read-only Go binding around an Ethereum contract,
// with pre-set call options.
type DecredSwapsCallerSession struct {
	Contract *DecredSwapsCaller // Generic contract caller binding to set the session for
	CallOpts bind.CallOpts      // Call options to use throughout this session
}

// DecredSwapsTransactorSession is an auto generated write-only Go binding around an Ethereum contract,
// with pre-set transact options.
type DecredSwapsTransactorSession struct {
	Contract     *DecredSwapsTransactor // Generic contract transactor binding to set the session for
	TransactOpts bind.TransactOpts      // Transaction auth options to use throughout this session
}

// DecredSwapsRaw is an auto generated low-level Go binding around an Ethereum contract.
type DecredSwapsRaw struct {
	Contract *DecredSwaps // Generic contract binding to access the raw methods on
}

// DecredSwapsCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract.
type DecredSwapsCallerRaw struct {
	Contract *DecredSwapsCaller // Generic read-only contract binding to access the raw methods on
}

// DecredSwapsTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract.
type DecredSwapsTransactorRaw struct {
	Contract *DecredSwapsTransactor // Generic write-only contract binding to access the raw methods on
}

// NewDecredSwaps creates a new instance of DecredSwaps, bound to a specific deployed contract.
func NewDecredSwaps(address common.Address, backend bind.ContractBackend) (*DecredSwaps, error) {
	contract, err := bindDecredSwaps(address, backend, backend, backend)
	if err != nil {
		return nil, err
	}
	return &DecredSwaps{DecredSwapsCaller: DecredSwapsCaller{contract: contract}, DecredSwapsTransactor: DecredSwapsTransactor{contract: contract}, DecredSwapsFilterer: DecredSwapsFilterer{contract: contract}}, nil
}

// NewDecredSwapsCaller creates a new read-only instance of DecredSwaps, bound to a specific deployed contract.
func NewDecredSwapsCaller(address common.Address, caller bind.ContractCaller) (*DecredSwapsCaller, error) {
	contract, err := bindDecredSwaps(address, caller, nil, nil)
	if err != nil {
		return nil, err
	}
	return &DecredSwapsCaller{contract: contract}, nil
}

// NewDecredSwapsTransactor creates a new write-only instance of DecredSwaps, bound to a specific deployed contract.
func NewDecredSwapsTransactor(address common.Address, transactor bind.ContractTransactor) (*DecredSwapsTransactor, error) {
	contract, err := bindDecredSwaps(address, nil, transactor, nil)
	if err != nil {
		return nil, err
	}
	return &DecredSwapsTransactor{contract: contract}, nil
}

// NewDecredSwapsFilterer creates a new log filterer instance of DecredSwaps, bound to a specific deployed contract.
func NewDecredSwapsFilterer(address common.Address, filterer bind.ContractFilterer) (*DecredSwapsFilterer, error) {
	contract, err := bindDecredSwaps(address, nil, nil, filterer)
	if err != nil {
		return nil, err
	}
	return &DecredSwapsFilterer{contract: contract}, nil
}

// bindDecredSwaps binds a generic wrapper to an already deployed contract.
func bindDecredSwaps(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) {
	parsed, err := abi.JSON(strings.NewReader(DecredSwapsABI))
	if err != nil {
		return nil, err
	}
	return bind.NewBoundContract(address, parsed, caller, transactor, filterer), nil
}

// Call invokes the (constant) contract method with params as input values and
// sets the output to result. The result type might be a single field for simple
// returns, a slice of interfaces for anonymous returns and a struct for named
// returns.
func (_DecredSwaps *DecredSwapsRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error {
	return _DecredSwaps.Contract.DecredSwapsCaller.contract.Call(opts, result, method, params...)
}

// Transfer initiates a plain transaction to move funds to the contract, calling
// its default method if one is available.
func (_DecredSwaps *DecredSwapsRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) {
	return _DecredSwaps.Contract.DecredSwapsTransactor.contract.Transfer(opts)
}

// Transact invokes the (paid) contract method with params as input values.
func (_DecredSwaps *DecredSwapsRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) {
	return _DecredSwaps.Contract.DecredSwapsTransactor.contract.Transact(opts, method, params...)
}

// Call invokes the (constant) contract method with params as input values and
// sets the output to result. The result type might be a single field for simple
// returns, a slice of interfaces for anonymous returns and a struct for named
// returns.
func (_DecredSwaps *DecredSwapsCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error {
	return _DecredSwaps.Contract.contract.Call(opts, result, method, params...)
}

// Transfer initiates a plain transaction to move funds to the contract, calling
// its default method if one is available.
func (_DecredSwaps *DecredSwapsTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) {
	return _DecredSwaps.Contract.contract.Transfer(opts)
}

// Transact invokes the (paid) contract method with params as input values.
func (_DecredSwaps *DecredSwapsTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) {
	return _DecredSwaps.Contract.contract.Transact(opts, method, params...)
}

// Swaps is a free data retrieval call binding the contract method 0xeb84e7f2.
//
// Solidity: function swaps(bytes32 ) view returns(uint256 initBlockNumber, uint256 refundBlockTimestamp, bytes32 secretHash, bytes32 secret, address initiator, address participant, uint256 value, uint8 state)
func (_DecredSwaps *DecredSwapsCaller) Swaps(opts *bind.CallOpts, arg0 [32]byte) (struct {
	InitBlockNumber      *big.Int
	RefundBlockTimestamp *big.Int
	SecretHash           [32]byte
	Secret               [32]byte
	Initiator            common.Address
	Participant          common.Address
	Value                *big.Int
	State                uint8
}, error) {
	var out []interface{}
	err := _DecredSwaps.contract.Call(opts, &out, "swaps", arg0)

	outstruct := new(struct {
		InitBlockNumber      *big.Int
		RefundBlockTimestamp *big.Int
		SecretHash           [32]byte
		Secret               [32]byte
		Initiator            common.Address
		Participant          common.Address
		Value                *big.Int
		State                uint8
	})
	if err != nil {
		return *outstruct, err
	}

	outstruct.InitBlockNumber = *abi.ConvertType(out[0], new(*big.Int)).(**big.Int)
	outstruct.RefundBlockTimestamp = *abi.ConvertType(out[1], new(*big.Int)).(**big.Int)
	outstruct.SecretHash = *abi.ConvertType(out[2], new([32]byte)).(*[32]byte)
	outstruct.Secret = *abi.ConvertType(out[3], new([32]byte)).(*[32]byte)
	outstruct.Initiator = *abi.ConvertType(out[4], new(common.Address)).(*common.Address)
	outstruct.Participant = *abi.ConvertType(out[5], new(common.Address)).(*common.Address)
	outstruct.Value = *abi.ConvertType(out[6], new(*big.Int)).(**big.Int)
	outstruct.State = *abi.ConvertType(out[7], new(uint8)).(*uint8)

	return *outstruct, err

}

// Swaps is a free data retrieval call binding the contract method 0xeb84e7f2.
//
// Solidity: function swaps(bytes32 ) view returns(uint256 initBlockNumber, uint256 refundBlockTimestamp, bytes32 secretHash, bytes32 secret, address initiator, address participant, uint256 value, uint8 state)
func (_DecredSwaps *DecredSwapsSession) Swaps(arg0 [32]byte) (struct {
	InitBlockNumber      *big.Int
	RefundBlockTimestamp *big.Int
	SecretHash           [32]byte
	Secret               [32]byte
	Initiator            common.Address
	Participant          common.Address
	Value                *big.Int
	State                uint8
}, error) {
	return _DecredSwaps.Contract.Swaps(&_DecredSwaps.CallOpts, arg0)
}

// Swaps is a free data retrieval call binding the contract method 0xeb84e7f2.
//
// Solidity: function swaps(bytes32 ) view returns(uint256 initBlockNumber, uint256 refundBlockTimestamp, bytes32 secretHash, bytes32 secret, address initiator, address participant, uint256 value, uint8 state)
func (_DecredSwaps *DecredSwapsCallerSession) Swaps(arg0 [32]byte) (struct {
	InitBlockNumber      *big.Int
	RefundBlockTimestamp *big.Int
	SecretHash           [32]byte
	Secret               [32]byte
	Initiator            common.Address
	Participant          common.Address
	Value                *big.Int
	State                uint8
}, error) {
	return _DecredSwaps.Contract.Swaps(&_DecredSwaps.CallOpts, arg0)
}

// Initiate is a paid mutator transaction binding the contract method 0xae052147.
//
// Solidity: function initiate(uint256 refundTimestamp, bytes32 secretHash, address participant) payable returns()
func (_DecredSwaps *DecredSwapsTransactor) Initiate(opts *bind.TransactOpts, refundTimestamp *big.Int, secretHash [32]byte, participant common.Address) (*types.Transaction, error) {
	return _DecredSwaps.contract.Transact(opts, "initiate", refundTimestamp, secretHash, participant)
}

// Initiate is a paid mutator transaction binding the contract method 0xae052147.
//
// Solidity: function initiate(uint256 refundTimestamp, bytes32 secretHash, address participant) payable returns()
func (_DecredSwaps *DecredSwapsSession) Initiate(refundTimestamp *big.Int, secretHash [32]byte, participant common.Address) (*types.Transaction, error) {
	return _DecredSwaps.Contract.Initiate(&_DecredSwaps.TransactOpts, refundTimestamp, secretHash, participant)
}

// Initiate is a paid mutator transaction binding the contract method 0xae052147.
//
// Solidity: function initiate(uint256 refundTimestamp, bytes32 secretHash, address participant) payable returns()
func (_DecredSwaps *DecredSwapsTransactorSession) Initiate(refundTimestamp *big.Int, secretHash [32]byte, participant common.Address) (*types.Transaction, error) {
	return _DecredSwaps.Contract.Initiate(&_DecredSwaps.TransactOpts, refundTimestamp, secretHash, participant)
}

// Redeem is a paid mutator transaction binding the contract method 0xb31597ad.
//
// Solidity: function redeem(bytes32 secret, bytes32 secretHash) returns()
func (_DecredSwaps *DecredSwapsTransactor) Redeem(opts *bind.TransactOpts, secret [32]byte, secretHash [32]byte) (*types.Transaction, error) {
	return _DecredSwaps.contract.Transact(opts, "redeem", secret, secretHash)
}

// Redeem is a paid mutator transaction binding the contract method 0xb31597ad.
//
// Solidity: function redeem(bytes32 secret, bytes32 secretHash) returns()
func (_DecredSwaps *DecredSwapsSession) Redeem(secret [32]byte, secretHash [32]byte) (*types.Transaction, error) {
	return _DecredSwaps.Contract.Redeem(&_DecredSwaps.TransactOpts, secret, secretHash)
}

// Redeem is a paid mutator transaction binding the contract method 0xb31597ad.
//
// Solidity: function redeem(bytes32 secret, bytes32 secretHash) returns()
func (_DecredSwaps *DecredSwapsTransactorSession) Redeem(secret [32]byte, secretHash [32]byte) (*types.Transaction, error) {
	return _DecredSwaps.Contract.Redeem(&_DecredSwaps.TransactOpts, secret, secretHash)
}

// Refund is a paid mutator transaction binding the contract method 0x7249fbb6.
//
// Solidity: function refund(bytes32 secretHash) returns()
func (_DecredSwaps *DecredSwapsTransactor) Refund(opts *bind.TransactOpts, secretHash [32]byte) (*types.Transaction, error) {
	return _DecredSwaps.contract.Transact(opts, "refund", secretHash)
}

// Refund is a paid mutator transaction binding the contract method 0x7249fbb6.
//
// Solidity: function refund(bytes32 secretHash) returns()
func (_DecredSwaps *DecredSwapsSession) Refund(secretHash [32]byte) (*types.Transaction, error) {
	return _DecredSwaps.Contract.Refund(&_DecredSwaps.TransactOpts, secretHash)
}

// Refund is a paid mutator transaction binding the contract method 0x7249fbb6.
//
// Solidity: function refund(bytes32 secretHash) returns()
func (_DecredSwaps *DecredSwapsTransactorSession) Refund(secretHash [32]byte) (*types.Transaction, error) {
	return _DecredSwaps.Contract.Refund(&_DecredSwaps.TransactOpts, secretHash)
}

@buck54321
Copy link
Member

buck54321 commented Mar 11, 2021

Here's a relevant snippet. We're looking good. I do kinda want to change the swaps mapping name to swap now though.

func (_DecredSwaps *DecredSwapsSession) Swaps(arg0 [32]byte) (struct {
	InitBlockNumber      *big.Int
	RefundBlockTimestamp *big.Int
	SecretHash           [32]byte
	Secret               [32]byte
	Initiator            common.Address
	Participant          common.Address
	Value                *big.Int
	State                uint8
}, error) {
	return _DecredSwaps.Contract.Swaps(&_DecredSwaps.CallOpts, arg0)
}

@JoeGruffins I edited your comment to add the ```Go formatting specifier to the code block. I thought that would be more clear from the diff, but it's not.

@chappjc
Copy link
Member

chappjc commented Mar 22, 2021

So there is a single contract that all traders use to make swaps? Calling it The One Swap Contract in #1021

Some questions:

  1. Are we aware of any instance of this contract being published on eth mainnet? I believe the following is one on Rinkeby, but only the bytecode is known by etherscan.io: https://rinkeby.etherscan.io/address/0x2661cbaa149721f7c5fab3fa88c1ea564a683631 (decomp result).

  2. Will we publish this contract before deployment or will the first user have to? e.g. The above contract appears to have been published in https://rinkeby.etherscan.io/tx/0xfffa27138fa9651c74d1e497f1fafc2c71c7528fddafda9a992809a3e68d7dd1

  3. What are the important pieces of info in each txn? Consider the most recent initiation txn, and corresponding redemption.

    In the init tx: "To" is the contract account. "Value" is evidently the value of the trade. "Input Data" is a blob that I think is the initiate method call that includes the secret hash (efc3fbdff...), the counterparty's account (83cc23...), and refund block number (init block is current best or mined block?). The refund address is gotten from the txn "from" address / msg.sender, similarly value value gotten from msg.value? State gets set to Filled by the initiate code.

    In the redeem tx: "From" is some regular account of the redeemer and which pays gas. "To" is the contract again but it indicates a transfer of the original init tx amount from the contract address to the address in "From" (internal txn of the contract account). "Value" is 0 as the change to the recipient's balance is a change of state from the execution of the txn. (State changes are: -0.5 from contract, +0.499933659 to the redeemer's from address, and the gas fee payment). "Input Data" is the redeem method call with the secret (6d45e4bf...) and it's hash (efc3fbdff...). Worth noting that the gas for the redeem is deducted from the redeemed amount (paid from the contract account balance) - initially I thought it was coming out of the From address in this redeem contract txn, but it's the internal from address (the contract account).

@chappjc
Copy link
Member

chappjc commented Mar 22, 2021

Also, with an absolute refundable block number, initBlockNumber is unused by the contract methods, and only for ensuring the init txn has reached the required confirmations, as you've indicated. If we want to embrace the smart contract approach to this instead of tracking the init tx confirmations, I wonder if the participate method should check the confirmation count of the Swap in Filled state, or perhaps an initConfirmCount method to make the intent clear unless actually using this data or method has a penalty. Can you clarify how the swap contract state data or methods can be used to do something like this (checking confirmations) without generating a transaction? Can light clients do read-only interactions with the contract at will like this at no cost?

Apart from the init tx confirmations question, the original contract used times and a duration from the init time instead, and I believe that does fit in our needs. That is, we want a certain amount of time to take actions regardless of how fast blocks are mined. I suppose I could be persuaded to accept an absolute block where refunds are allowed, if the statistical deviation from the target of say 8 or 20 hours is fairly low, like 20 minutes.

@JoeGruffins
Copy link
Member Author

So there is a single contract that all traders use to make swaps? Calling it The One Swap Contract in #1021

Yes.

1. Are we aware of any instance of this contract being published on eth mainnet? 

Not that I am aware of. It looks like it can be difficult to understand what some data means on the blockchain without seeing the .sol file: https://ethereum.stackexchange.com/questions/188/how-can-you-decompile-a-smart-contract
#1019 will make all the data visible on our simnet chains, will get it finished up asap, contracts should be relatively plug and play

2. Will we publish this contract before deployment or will the first user have to?  

We will.

3. What are the important pieces of info in each txn?  Consider the [most recent initiation txn](https://rinkeby.etherscan.io/tx/0x1e305e7c8e847a0adfd565df9a88c14471d74b2f629f75091213dc8a4159a6a7), and [corresponding redemption](https://rinkeby.etherscan.io/tx/0x214e0914b4c1efd16d45c69fd5ec90782e4c76875f77d1c69fda892ef6ab332a).
   In the init tx: "To" is the contract account. "Value" is evidently the value of the trade. "Input Data" is a blob that I think is the `initiate` method call that includes the secret hash (efc3fbdff...), the counterparty's account (83cc23...), and refund block number (init block is current best or mined block?).  The refund address is gotten from the txn "from" address / `msg.sender`, similarly value value gotten from `msg.value`?  State gets set to Filled by the initiate code.

Yes...
I believe init is the best block: https://docs.soliditylang.org/en/v0.8.2/units-and-global-variables.html but will test it out.

   In the redeem tx: "From" is some regular account of the redeemer and which pays gas. "To" is the contract _again_ but it indicates a transfer of the original init tx amount from the contract address to the address in "From" ([internal txn](https://rinkeby.etherscan.io/tx/0x214e0914b4c1efd16d45c69fd5ec90782e4c76875f77d1c69fda892ef6ab332a#internal) of the contract account). "Value" is 0 as the change to the recipient's balance is a [change of state](https://rinkeby.etherscan.io/tx/0x214e0914b4c1efd16d45c69fd5ec90782e4c76875f77d1c69fda892ef6ab332a#statechange) from the execution of the txn. (State changes are: -0.5 from contract, +0.499933659 to the redeemer's from address, and the gas fee payment).  "Input Data" is the `redeem` method call with the secret (6d45e4bf...) and it's hash (efc3fbdff...).  Worth noting that the gas for the redeem is deducted from the redeemed amount (paid from the contract account balance) - initially I thought it was coming out of the From address in this redeem contract txn, but it's the internal from address (the contract account).

I'm not sure. Everything I've read indicates that a contract cannot be used to pay gas fees. Do you have the .sol file for that contract?

Can you clarify how the swap contract state data or methods can be used to do something like this (checking confirmations) without generating a transaction? Can light clients do read-only interactions with the contract at will like this at no cost?

Clients, even light, can read at any time for free, no tx. We can also have the redeem fail if there are not enough confirmations, how many? Also, this version does away with "participate". I am not sure what the point of those methods were? You can either redeem/refund or you can't. The initiator decides the participator.

@buck54321
Copy link
Member

in the redeem tx: "From" is some regular account of the redeemer and which pays gas. "To" is the contract again but it indicates a transfer of the original init tx amount from the contract address to the address in "From"

Sort of. The "From" account is only checked against the address specified in the initiation tx as a validation step.

If we want to embrace the smart contract approach to ...

I think this is sort of an over-arching question that needs to be answered early. It appears that we can do this without sending any txids at all. You mention that you want to record the transaction, and my gut agrees, but part of me wonders why we should (other than the generating client). Unless we suspect that we can't trust the methods exposed by the contract, then there doesn't appear to be a compelling case for recording that info server-side or even conveying it to the counter-party.

We do probably need a confirmations(secretHash) method though.

That is, we want a certain amount of time to take actions regardless of how fast blocks are mined. I suppose I could be persuaded to accept an absolute block where refunds are allowed, if the statistical deviation from the target of say 8 or 20 hours is fairly low, like 20 minutes.

It looks like with Ethereum's block validation algorithm, there is a reasonably small restriction on block times that the network will propagate, so on the scales of hours using time will be safe to within about 15 minutes.

If we want to use time, in Solidity, we would check block.timestamp or now (alias) vs the init tx timestamp. Unfortunately, we are not recording that, and there is no way to get timestamp data for an arbitrary block in Solidity (based on this, at least). So if we do want to use time, we'll need to create a field in Swap and assign now in initiate.

@xaur
Copy link
Contributor

xaur commented Apr 4, 2021

Given how many times smart contracts have been pwned in the past, we might want to ask some ETH expert to review the Solidity code. The contract code above is quite short, without any loops and looks mostly declarative, but I'd like to be triple sure about its correctness.

@chappjc
Copy link
Member

chappjc commented Apr 5, 2021

We agree with that @xaur. We will also have to consider the contract in the broader context of atomic swap and dex. For instance, the current contract proposed in this issue has a preimage length vulnerability that was an issue with the earliest atomicswap contracts (pre-dex). That is, the redeem(bytes32 secret, bytes32 secretHash) method must verify the length of secret, which corresponds the secret length shared with all other asset's contracts. Namely, btc and dcr have a 520 bytes limit on data pushes (more for segwit data pushes I think), but I suspect that Solidity's shas256 allows for much larger inputs.

This gets to one fundamental different with the eth contract vs the btc or dcr contracts, which is that we the developers are making and auditing the swap contract, whereas with btc and dcr, the actual contract scripts are audited client-side. There's obviously still auditing of certain Swap data with the eth contract, but certain things are known / automatically pass if the contract being used turns out to be the one we published.

@JoeGruffins
Copy link
Member Author

For instance, the current contract proposed in this issue has a preimage length vulnerability that was an issue with the earliest atomicswap contracts (pre-dex). That is, the redeem(bytes32 secret, bytes32 secretHash) method must verify the length of secret, which corresponds the secret length shared with all other asset's contracts. Namely, btc and dcr have a 520 bytes limit on data pushes (more for segwit data pushes I think), but I suspect that Solidity's shas256 allows for much larger inputs.

Could you expand on this? I think the current redeem will only allow a 32 byte secret as input.

@chappjc
Copy link
Member

chappjc commented Apr 6, 2021

Ah, right I was reading this as bytes. You're right it ensures it will match the expected secret size because of the bytes32 type.

@JoeGruffins
Copy link
Member Author

JoeGruffins commented Jul 26, 2021

originally posted at #1019 (comment)

But I have been meaning to discuss other contract related concerns I've been pondering, ranging from specific lessons learned in recent weeks, to higher level decisions like using a single contract for EVERYONE. I personally think this is one of the mistakes made by not just thor but eth and defi in general -- putting all the eggs in one basket with a single fallible contract. It was a hard lesson for The DAO years ago, and again with the Thor Router. (don't just hack one person, hack all involved)

In UTXO-land we make a unique contract for each swap, and while I get that a single reusalble contract published by a third party can have utility in many ways including saving on tx fees, it does make me uneasy... from a security point of view as well as bringing a third party (devs / contract publishers) into the mix.

@JoeGruffins
Copy link
Member Author

JoeGruffins commented Aug 2, 2021

re: contract per swap

Thinking about this a bit more, I think the biggest barrier (after extra fees) is validating every contract.

If there's one contract, one can at any time use the same version of solidity to compile our contract.sol and compare the resulting byte code to the byte code in the original transaction and see that the code is what we say (I think).

However, if everyone has their own contract, I guess the only way to verify is to also have the correct version of solc installed on every dexc and rebuild the contract to check the byte code. I can imagine it taking a lot of code to get versions right and such.

afaik contract byte code cannot be reversed engineered to view what is doing what.

Related: https://ethereum.stackexchange.com/questions/188/how-can-you-decompile-a-smart-contract/238#238

@JoeGruffins
Copy link
Member Author

JoeGruffins commented Aug 2, 2021

On a related note, maybe we should hard code the version of solc used in the code. I don't think it's possible to know otherwise.

Related: https://www.shawntabrizi.com/ethereum/verify-ethereum-contracts-using-web3-js-and-solc/

@chappjc
Copy link
Member

chappjc commented Aug 2, 2021

Good calls @JoeGruffins. I agree that auditing the eth contract on-the-fly is next to impossible. That's probably opening up to more vulnerabilities than using a single contract for everybody like we're planning now.

We should continue to consider ways to limit the impact of an attack however. Multiple contracts, randomly choosing one? Hard-coded limits on certain amounts? Dunno.

EDIT: linking to the outcome of the ETHSwapV0 PR (#1019 (review)), where we identified and confirmed a vulnerability in the originally proposed contract in which the entire contract could be drained, not just one swap worth of ETH.

@chappjc
Copy link
Member

chappjc commented Aug 2, 2021

Another thing I expect we'd want to consider for the contract(s) is having it upgradable with a basic proxy like USDC does with delegatecall. See Read as Proxy and Write as Proxy: https://etherscan.io/token/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48?a=0x34c28c9e2e6832b3aff55bac588d6eba999dcdd0#writeContract
and
https://blog.coinbase.com/usdc-v2-upgrading-a-multi-billion-dollar-erc-20-token-b57cd9437096

At present for DCRDEX, this strikes me as unnecessary complexity, but potentially necessary in the long run.

EDIT: Forget that. I believe that would positively undermine the trustless nature of DCRDEX, and bring devs into an undesirably active role in the operation of a dex.

@JoeGruffins
Copy link
Member Author

JoeGruffins commented Aug 3, 2021

I think that delegate would break the ability to verify a contract. Whoever creates it will be able to switch out contracts without anyone noticing. My cursory impression is that we cannot use that anyway for security reasons.

Or, I suppose you could include the real contract's address as an argument and have that contract verify we are being proxied to what we think we are. But at that point, I can't imagine why we would use the proxy, just use the real address.

@chappjc
Copy link
Member

chappjc commented Aug 3, 2021

Yah, that's what I meant by "bring devs into an undesirably active role in the operation of a dex" in my edit, because they would be the admins that can upgrade at will. Definitely a bad idea.

@JoeGruffins
Copy link
Member Author

JoeGruffins commented Aug 3, 2021

Well, someone will have to deploy the contract anyway. I guess. It's not a whole lot different than what we are planning I believe.

@JoeGruffins
Copy link
Member Author

Consider adding public functions that aid in debugging from an explorer: #1019 (comment)

@JoeGruffins
Copy link
Member Author

I believe the initial discussion can be considered finished. We have a working contract deployed on testnet. Therefore, closing this initial discussion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants