-
Notifications
You must be signed in to change notification settings - Fork 100
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
multi: Add ETHSwapV0. #1019
Conversation
JoeGruffins
commented
Mar 19, 2021
dde1eaa
to
0a45cb0
Compare
0a45cb0
to
bc9890b
Compare
805e2a7
to
1d030df
Compare
Is this test failure #1018 ? |
1d030df
to
e93c9c1
Compare
e93c9c1
to
8e77378
Compare
You may rebase with PR #1078 in so we can give it a review. |
8e77378
to
223e952
Compare
// 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. |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Possibly realated...
I've spent a bit of time on this, but unable to stop this from happening so far.
There was a problem hiding this comment.
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.
Srry, stuck another one in front. |
223e952
to
3450a12
Compare
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
075782e
to
53d3cd1
Compare
53d3cd1
to
deebc82
Compare
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); |
There was a problem hiding this comment.
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 the receiving account transfer
could use up all the gas before storing the secret
, thus screwing over the counterparty, or (b)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).
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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 NVM, I see that these are like virtual entries when they don't exists, with the zero value of the type returned.swaps[secretHash]
should just be verified to be null
There was a problem hiding this 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:
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.
98adf63
to
775ae96
Compare
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. |
There was a problem hiding this 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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
// If the exploit worked, the test will fail here, with all funds | ||
// drained from the contract. | ||
if bal.Cmp(wantBal) != 0 { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
@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 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. It looks like the UI assumes atoms in a few places. Also, nothing else should work as of this pr, just initial set-up. |
There was a problem hiding this 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!
There was a problem hiding this 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)
Also thanks for your testing @vctt94. Very helpful, please keep it up. |
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. |
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. |
Even though it didn't follow best practice, theoretically permitting reentrant calls, the send instruction should limit gas in most cases. |