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.encodePacked
created some weird inconsistencies, butRiddle.sol
L136 specifically checked post packed results so that wasn't it - Finally, I figured I'd take a look at how the
ecrecover
was 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
v
intos
would 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 build
Create 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