unhackedctf

Share this post

schnoodle walkthrough (challenge 2)

unhackedctf.substack.com

schnoodle walkthrough (challenge 2)

how a faulty approval calculation led to a 100 ether hack

unhackedctf
Sep 10, 2022
5
Share this post

schnoodle walkthrough (challenge 2)

unhackedctf.substack.com

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.

congrats @yorkin for being the first to crack it

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.

please don’t steal mainnet funds :)

how to hack snood

tldr: here’s a link to my solution code

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 transferFrom function.

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 amount with _getStandardAmount(amount).

what is _getStandardAmount()? it takes in an amount and returns that number divided by _getReflectRate().

what’s _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 initialize function, 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 _spendAllowance function:

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..

wtf is 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.

the exploit

once we find this vulnerability, the exploit becomes easy. we’re able to transfer any SNOOD tokens we want using the transferFrom function.

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.

github link to the solution code.

we run the test and…

green light

ding ding ding.

what’s next?

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:

Share this post

schnoodle walkthrough (challenge 2)

unhackedctf.substack.com
Comments
TopNew

No posts

Ready for more?

© 2023 unhackedctf
Privacy ∙ Terms ∙ Collection notice
Start WritingGet the app
Substack is the home for great writing