Offensive Vyper - Challenge 1 - Unstoppable Auction
1) Challenge
Challenge created by @jtriley_eth.
2) Code Review
UnstoppableAuction.vy
contract (source code):
|
|
Contract main functions:
-
_handle_bid
holds the logic for handling the bid.bid
and the__default__
functions call it. In particular, multiple conditions need to be satisfied to set a new bid:- the contract balance is equal to the
total_deposit
variable plus the amount of the bid (line42
) - the auction is still valid (i.e. if it’s started and not ended) - line
44.
- the bidder is not the highest (line
48
); if it’s true, it checks if the current bid is greater than the highest bid (line49
). If the bidder is not the highest and the bid is not greater than the highest bid, the function fails with an assert message. - it increments the value of the
total_deposit
variable (line51
) - it increments the bid of the bidder (line
53
) - it sets the highest bid with the value of the current bid amount (line
55
) - it sets the highest bidder with the current bidder (line
57
)
- the contract balance is equal to the
-
withdraw
is used to withdraw the bid if it’s not the highest:- the caller is not the highest bidder (line
68
) - the contract balance equals the
total_deposit
value - lines
72-78
, update thedeposits
variable and sends the value deposited to the sender
- the caller is not the highest bidder (line
-
owner_withdraw
is used by the owner to withdraw the balance when the auction is ended -
bid
calls_handle_bid
-
__default__
, the fallback function, calls_handle_bid
Since our goal is to stop the auction from working, let’s focus our attention on the _handle_bid
function.
If we can make one of the conditions inside the _handle_bid
function permanently false, it will no longer complete its execution, causing a Denial-of-Service.
Among the conditions in _handle_bid
, only the condition at line 42
relies on comparing balance values that are stored in different places:
self.balance
is the contract balancetotal_deposit
is a variable that is updated when someone sends a new highest bid
When calling withdraw
, both self.balance
and total_deposit
are updated with the same value:
self.balance
is decreased because thesend
function is called (it transfers the balance of the contract tomsg.sender
)total_deposit
is reduced by the same amount that is sent to themsg.sender
We need to find a way to update self.balance
but not total_deposit
. This way, the condition assert self.balance == self.total_deposit + amount, "invalid balance"
will always be false.
We can transfer ETH to the contract by calling raw_call
(with some value amount) from a contract or using sendTransaction
from a ethers
script. However, the problem with this approach is that the fallback function defined in the contract will call the _handle_bid
.
The answer is YES. It’s possible to send the balance of a contract to another one by calling selfdestruct
. This way, no code is executed on the receiver contract, and thus the __default__
fallback will not be called. However, the balance of the contract that calls selfdestruct
will be transferred to the target contract. It means if we call selfdestruct
from a contract we control (that has some balance) and we set the receiver to be the auction contract, we will update the auction balance but not the total_deposit
variable, making the condition at line 42
false.
3) Solution
The solution consists of two steps:
- send some ETH to our contract so that its balance will be
> 0
- execute the
run
function of our contract that will callselfdestruct
specifying the receiver as the auction contract
unstoppable-auction.challenge.js
:
|
|
UnstoppableAuctionExploit.vy
:
|
|
You can find the complete code here and here.