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
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.
we run the test and…
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.
🫡