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

multi: Add ETHSwapV0. #1019

Merged
merged 6 commits into from
Aug 3, 2021
Merged

multi: Add ETHSwapV0. #1019

merged 6 commits into from
Aug 3, 2021

Conversation

JoeGruffins
Copy link
Member

Have the eth harness start with a contract. Add contract bindings and
basic calls to clients.

@JoeGruffins
Copy link
Member Author

Is this test failure #1018 ?

@chappjc
Copy link
Member

chappjc commented Jul 20, 2021

You may rebase with PR #1078 in so we can give it a review.

Comment on lines 9 to 12
// TODO: Occasionally tests will fail with "timed out" because transactions are
// not being mined. If one tx is not mined, following txs from the same account
// with a higher nonce also cannot be mined, so it causes all tests after to
// fail as well. Find out why the first tx fails to be broadcast.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be unrelated to #1130

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly realated...

ethereum/go-ethereum#21385

I've spent a bit of time on this, but unable to stop this from happening so far.

Copy link
Member Author

@JoeGruffins JoeGruffins Jul 28, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Headway on this. After adding better logging, the server was telling us:
TRACE[07-28|11:37:15.770] Discarding invalid transaction hash=5a1f01..40f83e err="nonce too low"

Which suggests that the light client sometimes has trouble discerning the correct nonce. Probably because we destroy its persistent data every test suite.

@JoeGruffins
Copy link
Member Author

You may rebase with PR #1078 in so we can give it a review.

Srry, stuck another one in front.

Comment on lines 72 to 89
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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Big 👍 for msg.sender and not tx.origin. But I wonder if we should outright prevent indirect calls by a proxy contract, say with something like require(tx.origin == msg.sender) to restrict the caller to being a regular "codeless" address.

Ref: https://github.com/ethereumbook/ethereumbook/blob/develop/09smart-contracts-security.asciidoc#txorigin-authentication

Copy link
Member Author

@JoeGruffins JoeGruffins Jul 26, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets do that.

The purpose of this pr is mostly to get more stuff hooked up. I would not be suprised at all if there are vulns in the contract. It is mostly a stripped down version of the original at https://github.com/decred/atomicswap/blob/2f6679b9150b2ef364cd7bc75a49c21a7d124a91/cmd/ethatomicswap/contract/src/contracts/AtomicSwap.sol

I make no claim that is safe at this point.

Copy link
Member

@chappjc chappjc Jul 26, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, we're working toward PoC. The work proposed to the atomicswap repo is absolutely the right starting point, and done by ETH oriented developers. I think we should all agree that none of this will be default-enabled on mainnet until much farther along in the process, and maybe not at all without outside consultation and extended testing... such is the nature of ETH. We definitely need to be clear about that.

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.

Just something I'd like to consider in the future. Nothing to hold up this work though since as you've said we're building the ETH framework for DCRDEX. The contract we make now is not meant to be the perfect solution for product. (EDIT: not meant to be)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting.

I hope you don't mine me quoting this for the contract issue.

Copy link
Member

@chappjc chappjc Jul 26, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, not at all. I meant to be explicit about this. This is a process and PR 1019 isn't the end-all.

@JoeGruffins JoeGruffins force-pushed the deploycontract branch 2 times, most recently from 075782e to 53d3cd1 Compare July 26, 2021 03:51
@JoeGruffins JoeGruffins marked this pull request as ready for review July 26, 2021 04:05
@JoeGruffins
Copy link
Member Author

So far unable to hit several errors that I was seeing in tests when using the same internal node directory. Please let me know of any random failures.

senderIsOrigin()
isRedeemable(secretHash, secret, msg.sender)
{
payable(msg.sender).transfer(swaps[secretHash].value);
Copy link
Member

@chappjc chappjc Jul 28, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it doesn't apply to us, but I was looking into send/transfer/call, and there is a recommendation to stop using call or transfer, and instead use call. https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now, https://consensys.github.io/smart-contract-best-practices/recommendations/#dont-use-transfer-or-send, and https://ethereum.stackexchange.com/a/78136

send/transfer being introduced after The DAO hack to mitigate reentrancy exploits seems to be seen as a half-measure at guarding reentrancy (by starving external calls of gas), plus the possibility of operations changing gas costs (see istanbul/SLOAD) makes transfer not recommended.

Although we're not making state updates to a "user balance" map, I'd be concerned that either (a) the transfer could use up all the gas before storing the secret, thus screwing over the counterparty, or (b) the receiving account payable function would call back to redeem and get paid again. I think prevailing advice for mitigating reentrancy is the checks-effects-interactions pattern, whereby you put external calls after all state changes (i.e. so that isRedeemable will prevent another send if state has already been updated before the external call that could call back to this function). This pattern seems to be preferred to a mutex these days (https://consensys.github.io/smart-contract-best-practices/known_attacks/#reentrancy).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like we need to update the state, i.e. swaps[secretHash].state = State.Redeemed; before sending funds and that should prevent any replay attacks here, so any amount of gas would be fine. ofc no expert tho.

Copy link
Member Author

@JoeGruffins JoeGruffins Jul 29, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah but wait, if sending then failed, the funds would be locked forever... Or would failure sending revert the state?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although we're not making state updates to a "user balance" map

I think we pretty much are, and in it's current state it looks like a redeemer or even refunder could indeed drain the contract using a replay attack with enough gas, which transfer, and it looks like send too, limits. The difference between the two being that transfer throws an exception on error while send just return false.

Copy link
Member

@chappjc chappjc Jul 29, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah but wait, if sending then failed, the funds would be locked forever... Or would failure sending revert the state?

That's what I thought. The txn would fail, and the external call be reverted. Guess it needs a test.
Hmm, I suppose issue (a) in my original comment isn't so much the concern as (b) draining the contract with a reentrant call.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm on it with a solution and a test.

Copy link
Member

@chappjc chappjc Jul 29, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice. But I'm far less concerned about transfer->call advice than about preventing reentrancy attacks, which I think we'd accomplish with updating state before external calls. In it's current form an attacker could indeed drain the entire contract given enough gas and their own contract with a fallback function that called back to our contract's redeem. This is just the kind of thing I meant about defi liking to put all the eggs in one basket, but I don't have a better suggestion at the moment (aside from each trade publishing a new contract like we do with utxos).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took a deep delve into the contract and am feeling pretty confident now.

}

modifier isNotInitiated(bytes32 secretHash) {
require(swaps[secretHash].state == State.Empty);
Copy link
Member

@chappjc chappjc Jul 28, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curious why checking like this when I feel swaps[secretHash] should just be verified to be null NVM, I see that these are like virtual entries when they don't exists, with the zero value of the type returned.

Copy link
Member

@vctt94 vctt94 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @JoeGruffins, So when I try to add the alpha ethereum wallet from harness, I got the following:

image

Not sure if I am doing something wrong here, have you seen this?

Have the eth harness start with a contract. Add contract bindings and
basic calls to clients.
@JoeGruffins
Copy link
Member Author

Have not looked into vctt's issue yet but will soon. It should not be pointed to any active nodes though, it should create a node at an empty location and reuse that.

Copy link
Member

@chappjc chappjc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome work doing a PoC for the reentrant contract vulnerability. Just some conceptual questions about testing the vuln contract, and the possibility for public read-only debugging methods.

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

// senderIsOrigin ensures that this contract cannot be used by other
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is "other contracts" is really "proxy contracts" since there still seems to be no prohibition on the sender's account type (contract vs. codeless acct). Or... would a contract calling the swapper contract imply there was a regular codeless account as the origin this making any contract caller a proxy contract?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At present, I don't think contracts can actively do anything. They need a normal account to perform methods. The original account is always a human/normal account afaict. I really tried to find a way to spoof origin in the reentry attack but could not. If it is possible it is not common knowledge.

// isRefundable checks that a swap can be refunded. The requirements are
// the initiator is msg.sender, the state is Filled, and the block
// timestamp be after the swap's stored refundBlockTimestamp.
modifier isRefundable(bytes32 secretHash) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was removing the address arg from isRefundable and isRedeemable strictly necessary or did it have significant security improvements or did it become redundant with the origin/sender checks? Seems allowing a third party or even a block explorer to debug would be nice. Actually, with modifier, are methods not going to be read only interactions that you can do without txn? Sorry for the naive questions here, but I'm just thinking about the possibility for methods that you can just hit with say etherescan.io's Read Contract feature for debugging.

Copy link
Member Author

@JoeGruffins JoeGruffins Aug 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reasoning for removing was simply because it didn't seem necessary as we are just sending msg.sender anyway. So, for simplicity.

I have not used etherscan.io's read contract feature... I will go check it out. However, msg.sender should be public as part of the initial transaction?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

e.g. some random contract https://etherscan.io/address/0xb1e9157c2fdcc5a856c8da8b2d89b6c32b3c1229#readContract

You can do stuff like balanceOf(account) in the explorer without messing with transactions

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like you can submit code to get some of that debugging. You can see that this contract is not set up to do that: https://etherscan.io/address/0x7f3a4085dcdccd5d52249dbf604ba9236b433040#code

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could make some separate public view functions that inform about states, and so would be visible and useable by an explorer. I read somewhere that public functions shouldn't call other public functions. Probably because of vulnerabilities like reentry attack creeping in undetected. Will look into it some more, but separate public functions for debugging like you say should be doable and fine, if you think it would be good.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sure we'll want such methods, and the contract source will be published and probably submitted too, but it's not something to hold up this PR anyway.
I expect these separate public functions won't add to gas cost for regular swaps, just the tx that publishes the actual contract, so I figure it's alright to add any such functions to the contract interface, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think that the gas cost for the initial contract would be higher, probably not so much higher, but it shouldn't affect other methods at all.

Comment on lines 812 to 814
// If the exploit worked, the test will fail here, with all funds
// drained from the contract.
if bal.Cmp(wantBal) != 0 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So to see the exploit actually hit these paths, you'd make bindings for the vulnerable contract like

abigen --sol ./reentryattack/VulnerableToReentryAttack.sol --pkg eth --out contract.go

and then run the test, which would be deploying the vulnerable contract?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You would then need to replace the contract bin hex in the test harness with the created contract.go, minus the leading 0x, and restart the harness.

Currently we use the contract deployed in the harness to test. We could also deploy a new contract per test.

I could add a test that also deploys this contract separately to test that the vulnerability contract does work on a vulnerable contract. I didn't know if that was going too far though, as a use of time I mean.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, --out should point to the contract in dex/networks/eth. I actually meant to write a small Readme, so I think I'll do that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to test the vuln contact forever. Just wondering

@JoeGruffins
Copy link
Member Author

JoeGruffins commented Aug 3, 2021

@vctt94 This should work now. I wasn't able to observe your issue specifically, but there was some missing logic for creating an account. Added a very temporary function to add a testing account that has funds. I have not tried on testnet, but anyway you should point the node to an empty directory, default should be fine, which is $HOME/.dexethclient

I notice a display issue that I won't try to correct here, but Balance is in gwei which is 1e9th of an eth. This should say 5,000 eth.
notatoms

It looks like the UI assumes atoms in a few places.

Also, nothing else should work as of this pr, just initial set-up.

Copy link
Member

@vctt94 vctt94 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey joe, yeah, it does create a new account now, on an empty dir, but yeah not many UI things working right now.

Will give it another round of tests later. Good job on this PR so far!

@chappjc chappjc added this to the 0.3 milestone Aug 3, 2021
Copy link
Member

@chappjc chappjc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. This PR was super productive IMO. I think we've learned a ton about ETH in general from you @JoeGruffins and about contract security. Pretty cool we were actually able to drain the proposed (but modified) contract from decred/atomicswap#76 because it also did not set state prior to making external calls (https://github.com/decred/atomicswap/pull/76/files#diff-bbbe323063803d4430a2f007b2e39e9cdefae18521d8d28664686728278db38aR158-R161)

@chappjc
Copy link
Member

chappjc commented Aug 3, 2021

Also thanks for your testing @vctt94. Very helpful, please keep it up.

@chappjc chappjc merged commit a7046c6 into decred:master Aug 3, 2021
@JoeGruffins
Copy link
Member Author

OK, but out of curiosity, is there no rpc to create a new account and return the address and private key?

There is! But for testing it's convenient to have funds, and for mainnet I think we will use an account based on the app seed so it can be recreated.

@xaur
Copy link
Contributor

xaur commented Sep 5, 2021

we were actually able to drain the proposed (but modified) contract from decred/atomicswap#76 because it also did not set state prior to making external calls

If there is a vuln in that PR it might be worth posting a summary in it. The PR looks dead but just in case.

@chappjc
Copy link
Member

chappjc commented Sep 5, 2021

Even though it didn't follow best practice, theoretically permitting reentrant calls, the send instruction should limit gas in most cases.
Not as bad as it could have been.

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

Successfully merging this pull request may close these issues.

5 participants