Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 61 additions & 1 deletion crates/precompiles/src/tip20/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -551,14 +551,15 @@ impl TIP20Token {
);

// 4. Validate ECDSA signature
// Only v=27/28 is accepted; v=0/1 is intentionally NOT normalized (see TIP-1004 spec).
if call.v != 27 && call.v != 28 {
return Err(TIP20Error::invalid_signature().into());
}
let parity = call.v == 28;
let sig = Signature::from_scalars_and_parity(call.r, call.s, parity);
let recovered = alloy::consensus::crypto::secp256k1::recover_signer(&sig, digest)
.map_err(|_| TIP20Error::invalid_signature())?;
if recovered != call.owner {
if recovered.is_zero() || recovered != call.owner {
return Err(TIP20Error::invalid_signature().into());
}

Expand Down Expand Up @@ -2600,5 +2601,64 @@ pub(crate) mod tests {
Ok(())
})
}

#[test]
fn test_permit_zero_address_recovery_reverts() -> eyre::Result<()> {
let PermitFixture {
mut storage,
admin,
spender,
..
} = PermitFixture::new();

StorageCtx::enter(&mut storage, || {
let mut token = TIP20Setup::create("Test", "TST", admin).apply()?;

let result = token.permit(ITIP20::permitCall {
owner: Address::ZERO,
spender,
value: U256::from(1000),
deadline: U256::MAX,
v: 27,
r: B256::ZERO,
s: B256::ZERO,
});

assert!(matches!(
result,
Err(TempoPrecompileError::TIP20(TIP20Error::InvalidSignature(_)))
));

Ok(())
})
}

#[test]
fn test_permit_domain_separator_changes_with_chain_id() -> eyre::Result<()> {
let PermitFixture { admin, .. } = PermitFixture::new();

let mut storage_a = setup_t2_storage();
let mut storage_b =
HashMapStorageProvider::new_with_spec(CHAIN_ID + 1, TempoHardfork::T2);

let ds_a = StorageCtx::enter(&mut storage_a, || {
TIP20Setup::create("Test", "TST", admin)
.apply()?
.domain_separator()
})?;

let ds_b = StorageCtx::enter(&mut storage_b, || {
TIP20Setup::create("Test", "TST", admin)
.apply()?
.domain_separator()
})?;

assert_ne!(
ds_a, ds_b,
"domain separator must change when chainId changes"
);

Ok(())
}
}
}
3 changes: 2 additions & 1 deletion tips/tip-1004.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,8 @@ The implementation must:
2. Retrieve the current nonce for `owner` and use it to construct the `structHash` and `digest`
3. Increment `nonces[owner]`
4. Validate the signature:
- First, use `ecrecover` to recover a signer address from the digest
- The `v` parameter must be `27` or `28`. Values of `0` or `1` are **not** normalized and will revert with `InvalidSignature`. Callers using signing libraries that produce `v ∈ {0, 1}` must add `27` before calling `permit`.
- Use `ecrecover` to recover a signer address from the digest
- If `ecrecover` returns a non-zero address that equals `owner`, the signature is valid (EOA case)
- Otherwise, revert with `InvalidSignature`
5. Set `allowance[owner][spender] = value`
Expand Down
Loading