Got dragged into the Base's 2023 challenge while perusing twitter on a Saturday afternoon. Figured I clean up the solution and post publicly for anyone who's curious how it was solved.
TLDR: Signatures can be 64bits or 65bits long. If you have a quirky implementation of ECDSA.sol (like Base did on purpose) you can end up with two different signatures that result in the same signer.
Challenge description: https://www.coinbase.com/bounty/ethdenver23
Challenges 1 & 2 were quite straight-forward and do not need any explanation apart from the actual code in Solution.s.sol.
The third challenge, however, was a tough nut to crack. Here's the logical steps that led me to finding the solution (honestly about ~2hrs):
- Realize that the only way to pass is to submit two different signatures that result in the same signer
- Actual ECDSA collisions are not practical - we'd have bigger issues if they were
- Considered maybe
abi.encodePackedcreated some weird inconsistencies, butRiddle.solL136 specifically checked post packed results so that wasn't it - Finally, I figured I'd take a look at how the
ecrecoverwas done - If there are any weird tricks, they will show in the diff between OpenZepellin's ECDSA.sol contract.
- Boom boom -> Base challenge added another branch where signature can either be 64 or 65 bytes long.
- From here on it was pretty quick to realize that packing
vintoswould result in a different signature - There were a couple hurdles you'd need to jump through if you're not familiar signature signing -> e.g. ECSDA.sol assumes v is 0 or 1 and then adds 27.
git submodule update --init --recursive
forge buildCreate a local .env file with the below vars:
PRIVATE_KEY=
PUBLIC_ADDRESS=
BASE_GOERLI_RPC_URL='https://goerli.base.org'Then run the Solution.s.sol script to complete all three challenges in one go:
source .env
forge script script/Solution.s.sol:Solution --rpc-url $BASE_GOERLI_RPC_URL --broadcast --verify -vvvv