# Schnorr Sigs as a Safe Module ![safeSchnorr](https://hackmd.io/_uploads/SyTicF5eyl.jpg) Schnorr signatures come with big potential but limited support in the Ethereum ecosystem. Here's a reminder of the advantages they have in a multisignature setup: * one EVM address signer - even if your multisig is composed of 10 signers, on-chain that is represented by a single EVM address. You get privacy as on-chain nobody knows who the real signers are * gas efficiency - all the signatures are aggregated into one off-chain, allowing the user to send the final, aggregated one on-chain. Verification is handled by ecrecover (~3000 gas) + a bit of math computation beforehand, bringing the total gas cost to ~3300 gas. Nevertheless, they are not so popular and probably the main reason for this is the lack of hardware support and the difficult off-chain setup you need to handle them. Here, I'll demonstrate how to enable and use these schnorr signatures as a Safe module. [Here' the github repository](https://github.com/borislav-itskov/safe-schnorkell) #### TLDR [Here's a link to the solidity contract if you don't want to waste your time with words](https://github.com/borislav-itskov/safe-schnorkell/blob/main/contracts/SafeSchnorr.sol) ## Safe module ![safe](https://hackmd.io/_uploads/HkmG_F5xkg.png) If you're new to [Safe](https://safe.global/), you have to know two things about it: * it's a Smart contract wallet * anyone is free to write modules for it and anyone that wants to use a module, can enable it for its account We're going to tap in its `execTransactionFromModule` metod to execute transactions after the schnorr signature validation ## On-chain Verification This is the standard Schnorr signature verification, tapping into `ecrecover`: ``` // secp256k1 group order uint256 internal constant Q = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141; function ecrecoverSchnorr( bytes32 commitment, bytes calldata signature ) public pure returns (address) { // Based on https://hackmd.io/@nZ-twauPRISEa6G9zg3XRw/SyjJzSLt9 // You can use this library to produce signatures: https://github.com/borislav-itskov/schnorrkel.js // px := public key x-coord // e := schnorr signature challenge // s := schnorr signature // parity := public key y-coord parity (27 or 28) // last uint8 is for the Ambire sig mode - it's ignored (bytes32 px, bytes32 e, bytes32 s, uint8 parity) = abi.decode( signature, (bytes32, bytes32, bytes32, uint8) ); // ecrecover = (m, v, r, s); bytes32 sp = bytes32(Q - mulmod(uint256(s), uint256(px), Q)); bytes32 ep = bytes32(Q - mulmod(uint256(e), uint256(px), Q)); require(sp != bytes32(Q)); // the ecrecover precompile implementation checks that the `r` and `s` // inputs are non-zero (in this case, `px` and `ep`), thus we don't need to // check if they're zero. address R = ecrecover(sp, parity, px, ep); require(R != address(0), "SV_ZERO_SIG"); require( e == keccak256(abi.encodePacked(R, uint8(parity), px, commitment)), "SV_SCHNORR_FAILED" ); return address(uint160(uint256(px))); } ``` Mind you, this is not enough. This is generally good enough to verify schnorr signatures but there are a couple of things missing: * at the end, you're returned the schnorr EVM address. We have to check if this address is actually authorized to do actions on behalf of the safe * it is not replay protected So we'll have to a few additional things in the module to assure all's good ## Safe protection When deploying the module, we'll set a few properties: ``` uint256 public nonce; struct Call { address to; uint256 value; bytes data; } constructor(Safe _safe, address _signer) { signer = _signer; safe = _safe; } ``` The signer is the schnorr EVM address with privileges to execute transactions from the safe. And the safe address is... well, the safe address this module will be enabled for. So in our example, each Safe will have to deploy his safe schnorr module. And finally, the safe will have to enable it through `enableModule` We construct the execute function that will allow this to flourish: ``` function execute(Call[] calldata calls, bytes calldata signature) external { // prevent reEntry uint256 currentNonce = nonce; nonce = currentNonce + 1; // prevent replays by reconstructing the hash with // safe addr, module addr, chainId and nonce bytes32 commitment = keccak256( abi.encode( address(safe), address(this), block.chainid, currentNonce, calls ) ); address retrievedSigner = ecrecoverSchnorr(commitment, signature); require(retrievedSigner == signer, "INSUFFICIENT_PRIVILEGE"); // execute the batch uint256 len = calls.length; for (uint256 i = 0; i < len; i++) { Call memory call = calls[i]; safe.execTransactionFromModule( call.to, call.value, call.data, // forbid delegate calls, too unpredictable Enum.Operation.Call ); } } ``` Let's point out the basics: * we increment the nonce at the beginning to prevent re-entrancy attacks * we construct a commitment from: the safe addr, the module addr, the chain id, the current nonce and finally, the calls we're going to execute. This makes sure that the signature cannot be reused twice, either for this safe or another, neither for other chains * after performing the schnorr validation, we make sure the signer actually is the one set in the constructor (retrievedSigner == signer) And that's it! What remains is the execution which we perform by calling `safe.execTransactionFromModule` for all the calls in the commitment. All of this is a single transaction so remember, if one call is invalid, all the calls will revert ## Closing thoughts This is how you could implement Schnorr in a Safe. What can you use it for? Currently, mainly for a backup in case something happens to the original signers. Be wary that enabling a module in a Safe bypasses the original multisig setup. So proceed only if you know what you're doing. [You can use this library to craft the schnorr signatures](https://www.npmjs.com/package/@borislav.itskov/schnorrkel.js)