Discover more from unhackedctf
schnoodle walkthrough (challenge 2)
how a faulty approval calculation led to a 100 ether hack
i’m impressed, anon.
i gave you an easy assignment the first week. i wanted to make sure your abilities were sharp, and they were.
but this week i stepped it up, and you stepped up right along with me.
clearly your intelligence is unmatched. but this quest isn’t just about big brains. it’s about big hearts too. in that department, i still have my concerns.
how to hack snood
many of you struggled with this one, so let’s dive into the schnoodle code to figure out the exploit.
in ERC777Upgradeable, we find a pretty typical
this function uses the
_spendAllowance function to validate that the caller has sufficient allowance to make the transaction.
in the typical implementation,
_spendAllowance checks the allowance given to the spender (msg.sender), confirms that it’s greater than the amount, and reduces the current allowance by the amount.
if the allowance is less than the amount, the transaction reverts. this serves as validation that
transferFrom can only be called by users with sufficient allowance for the transfer.
in this typical architecture,
_spendAllowance is called with amount as the final parameter. but schnoodle uses a reflective algorithm for tokens, so to override this behavior, it overrides the
_spendAllowance function and replaces
_getStandardAmount()? it takes in an amount and returns that number divided by
_getReflectRate()? it returns
super.totalSupply() / totalSupply().
and, finally, what are those two different total supplies?
super.totalSupply() is the totalSupply from the ERC777Upgradeable contract. if we check that contract, we can see it’s only increased and decreased in the mint and burn functions, so it’s basically a count of the net minted tokens.
totalSupply() is the totalSupply from the SchnoodleV9Base contract. it’s initially set to initialTokens (in the
initializefunction, when the first tokens are minted), and then updates as future tokens are minted and burned (with some slight changes based on reflective algorithm, but we can ignore that here).
whew! so, after all this, we end up with the following formula for the amount that gets submitted to the
amount * (initialTokens + net mints after initializing) / net mints
seems like it should be safe, right? there’s just one problem. if we look at the
initialize function, it’s not quite as we’d expect it to be..
MAX - (MAX % totalSupply())?
that’s the number of tokens we’re asking the base contract to mint! but that isn’t right. if we’re trying to keep the total supplies lined up properly, we should be minting
initialTokens * 10 ** decimals().
instead, we’re minting an EXTREMELY high number, somewhere close to 2 ** 256!
this error means that super.totalSupply() is very high, which trickles through our calculation…
if super.totalSupply() is huge, then _getReflectRate() will be huge.
if _getReflectRate() is huge, then _getStandardAmount() will equal 0 for any input amount lower than _getReflectRate().
uh oh. this means that any call to
_spendAllowance for an amount less than the enormous
_getReflectRate() value will replace the amount with 0 when it checks the allowance, pass the check, and then continue on with the transfer for the original amount. bad news.
once we find this vulnerability, the exploit becomes easy. we’re able to transfer any SNOOD tokens we want using the
but we don’t just want the SNOOD. we want ETH.
first, we empty all the SNOOD (except 1 token) from the uniswap pool.
then we call
uniswap.sync() to have uniswap reprice the pair of assets based on the pair balances. since there is only 1 SNOOD token, it’s valued extremely highly in ETH terms.
finally, we swap all the SNOOD we stole for ETH (saving 1 SNOOD, because of a quirk in how
uniswap.swap() is implemented), emptying the ETH from the pool.
we run the test and…
ding ding ding.
we’re doing good work together, anon.
but the hacks we’ve been focused on have been small potatoes. we’re never going to save defi at this rate.
next week, we’ll turn our attention to something larger.
watch your inbox monday morning for more details.
get the next challenge straight to your inbox: