diff --git a/packages/protocol-kit/src/Safe.ts b/packages/protocol-kit/src/Safe.ts index e83d79076..ab47843dd 100644 --- a/packages/protocol-kit/src/Safe.ts +++ b/packages/protocol-kit/src/Safe.ts @@ -25,7 +25,6 @@ import FallbackHandlerManager from './managers/fallbackHandlerManager' import GuardManager from './managers/guardManager' import ModuleManager from './managers/moduleManager' import OwnerManager from './managers/ownerManager' -import SignatureManager from './managers/signatureManager' import { AddOwnerTxParams, ConnectSafeConfig, @@ -44,7 +43,12 @@ import { isSafeMultisigTransactionResponse, sameString } from './utils' -import { generatePreValidatedSignature } from './utils/signatures/utils' +import { + buildSignature, + generateEIP712Signature, + generatePreValidatedSignature, + generateSignature +} from './utils/signatures/utils' import EthSafeTransaction from './utils/transactions/SafeTransaction' import { SafeTransactionOptionalProps } from './utils/transactions/types' import { @@ -54,6 +58,7 @@ import { } from './utils/transactions/utils' import { isSafeConfigWithPredictedSafe } from './utils/types' import { + getCompatibilityFallbackHandlerContract, getMultiSendCallOnlyContract, getProxyFactoryContract, getSafeContract @@ -67,7 +72,8 @@ class Safe { #moduleManager!: ModuleManager #guardManager!: GuardManager #fallbackHandlerManager!: FallbackHandlerManager - signatures!: SignatureManager + + #MAGIC_VALUE = '0x1626ba7e' /** * Creates an instance of the Safe Core SDK. @@ -121,14 +127,6 @@ class Safe { this.#ethAdapter, this.#contractManager.safeContract ) - - this.signatures = new SignatureManager( - this.#ethAdapter, - this.#contractManager.safeContract, - contractNetworks - ) - - await this.signatures.init() } /** @@ -509,9 +507,10 @@ class Safe { * @param hash - The hash to sign * @returns The Safe signature */ - // TODO: Evaluate the method removal as it can be use4d through the new Signatures class async signTransactionHash(hash: string): Promise { - return this.signatures.signEIP191Message(hash) + const signature = await generateSignature(this.#ethAdapter, hash) + + return signature } /** @@ -522,16 +521,17 @@ class Safe { * @returns The Safe signature */ async signTypedData( - safeTransaction: SafeTransaction, + data: SafeTransaction | string, methodVersion?: 'v3' | 'v4' ): Promise { const safeEIP712Args: SafeEIP712Args = { safeAddress: await this.getAddress(), safeVersion: await this.getContractVersion(), chainId: await this.getEthAdapter().getChainId(), - data: safeTransaction.data + data: typeof data === 'string' ? data : data.data } - return this.signatures.signEIP712Message(safeEIP712Args, methodVersion) + + return generateEIP712Signature(this.#ethAdapter, safeEIP712Args, methodVersion) } /** @@ -1226,6 +1226,104 @@ class Safe { return transactionBatch } + + /** + * Call the getMessageHash method of the Safe CompatibilityFallbackHandler contract + * @param messageHash The hash of the message to be signed + * @returns Returns the hash of a message to be signed by owners + * @link https://github.com/safe-global/safe-contracts/blob/8ffae95faa815acf86ec8b50021ebe9f96abde10/contracts/handler/CompatibilityFallbackHandler.sol#L26-L28 + */ + getMessageHash = async (messageHash: string): Promise => { + if (!this.#contractManager.safeContract) { + throw new Error('Safe is not deployed') + } + + const safeAddress = await this.getAddress() + const safeVersion = + (await this.#contractManager.safeContract.getVersion()) ?? DEFAULT_SAFE_VERSION + const chainId = await this.#ethAdapter.getChainId() + + const compatibilityFallbackHandlerContract = await getCompatibilityFallbackHandlerContract({ + ethAdapter: this.#ethAdapter, + safeVersion, + customContracts: this.#contractManager.contractNetworks?.[chainId] + }) + + const data = compatibilityFallbackHandlerContract?.encode('getMessageHash', [messageHash]) + + const safeMessageHash = await this.#ethAdapter.call({ + from: safeAddress, + to: safeAddress, + data: data || '0x' + }) + + return safeMessageHash + } + + /** + * Signs a hash using the current signer account. + * + * @param hash - The hash to sign + * @returns The Safe signature + */ + async signMessageHash(hash: string, isSmartContract = false): Promise { + const signature = await generateSignature(this.#ethAdapter, hash) + + if (isSmartContract) { + const safeAddress = await this.getAddress() + return new EthSafeSignature(safeAddress, signature.data, isSmartContract) + } + + return signature + } + + /** + * Call the isValidSignature method of the Safe CompatibilityFallbackHandler contract + * @param messageHash The hash of the message to be signed + * @param signature The signature to be validated or '0x'. You can pass + * 1) An array of SafeSignature. In this case the signatures will be concatenated for validation + * 2) The concatenated signatures + * 3) '0x' if you want to validate an onchain message (Initialized by default) + * @returns A boolean indicating if the signature is valid + * @link https://github.com/safe-global/safe-contracts/blob/main/contracts/handler/CompatibilityFallbackHandler.sol + */ + isValidSignature = async ( + messageHash: string, + signature: SafeSignature[] | string = '0x' + ): Promise => { + if (!this.#contractManager.safeContract) { + throw new Error('Safe is not deployed') + } + + const safeAddress = await this.getAddress() + const safeVersion = + (await this.#contractManager.safeContract.getVersion()) ?? DEFAULT_SAFE_VERSION + const chainId = await this.#ethAdapter.getChainId() + + const compatibilityFallbackHandlerContract = await getCompatibilityFallbackHandlerContract({ + ethAdapter: this.#ethAdapter, + safeVersion, + customContracts: this.#contractManager.contractNetworks?.[chainId] + }) + + const data = compatibilityFallbackHandlerContract.encode('isValidSignature(bytes32,bytes)', [ + messageHash, + signature && Array.isArray(signature) ? buildSignature(signature) : signature + ]) + + try { + const isValidSignatureResponse = await this.#ethAdapter.call({ + from: safeAddress, + to: safeAddress, + data: data || '0x' + }) + + return isValidSignatureResponse.slice(0, 10).toLowerCase() === this.#MAGIC_VALUE + } catch (error) { + console.error(error) + return false + } + } } export default Safe diff --git a/packages/protocol-kit/src/managers/signatureManager.ts b/packages/protocol-kit/src/managers/signatureManager.ts deleted file mode 100644 index c33d0c891..000000000 --- a/packages/protocol-kit/src/managers/signatureManager.ts +++ /dev/null @@ -1,222 +0,0 @@ -import { - CompatibilityFallbackHandlerContract, - EthAdapter, - SafeContract, - SafeSignature, - SafeEIP712Args -} from '@safe-global/safe-core-sdk-types' -import { getCompatibilityFallbackHandlerContract } from '../contracts/safeDeploymentContracts' -import { ContractNetworksConfig } from '../types' -import { - generateSignature, - generateEIP712Signature, - EthSafeSignature, - buildSignatureBytes -} from '../utils' -import { DEFAULT_SAFE_VERSION } from '../contracts/config' -import { ethers } from 'ethers' - -/** - * @class SignatureManager - * The SignatureManager class is responsible to provide the tools to generate and validate signatures - * It can be accessed through the Safe instance `safeSdk.signature` - */ -class SignatureManager { - #ethAdapter: EthAdapter - #safeContract?: SafeContract - #contractNetworks?: ContractNetworksConfig - #fallbackHandler?: CompatibilityFallbackHandlerContract - - #MAGIC_VALUE = '0x1626ba7e' - - constructor( - ethAdapter: EthAdapter, - safeContract?: SafeContract, - contractNetworks?: ContractNetworksConfig - ) { - this.#ethAdapter = ethAdapter - this.#safeContract = safeContract - this.#contractNetworks = contractNetworks - } - - /** - * Initialize the SignatureManager - * This method should be called before using any of the SignatureManager methods. It will fetch the Safe CompatibilityFallbackHandler contract - */ - async init() { - const safeVersion = (await this.#safeContract?.getVersion()) ?? DEFAULT_SAFE_VERSION - const chainId = await this.#ethAdapter.getChainId() - - const compatibilityFallbackHandlerContract = await getCompatibilityFallbackHandlerContract({ - ethAdapter: this.#ethAdapter, - safeVersion, - customContracts: this.#contractNetworks?.[chainId] - }) - - this.#fallbackHandler = compatibilityFallbackHandlerContract - } - - /** - * Call the isValidSignature method of the Safe CompatibilityFallbackHandler contract - * @param messageHash The hash of the message to be signed - * @param signature The signature to be validated or '0x'. You can pass - * 1) An array of SafeSignature. In this case the signatures will be concatenated for validation - * 2) The concatenated signatures - * 3) '0x' if you want to validate an onchain message (Initialized by default) - * @returns A boolean indicating if the signature is valid - * @link https://github.com/safe-global/safe-contracts/blob/main/contracts/handler/CompatibilityFallbackHandler.sol - */ - isValidSignature = async ( - messageHash: string, - signature: SafeSignature[] | string = '0x' - ): Promise => { - const safeAddress = this.#safeContract?.getAddress() || '' - - const data = this.#fallbackHandler?.encode('isValidSignature(bytes32,bytes)', [ - messageHash, - signature && Array.isArray(signature) ? this.buildSignature(signature) : signature - ]) - - try { - const isValidSignatureResponse = await this.#ethAdapter.call({ - from: safeAddress, - to: safeAddress, - data: data || '0x' - }) - - return isValidSignatureResponse.slice(0, 10).toLowerCase() === this.#MAGIC_VALUE - } catch (error) { - console.error(error) - return false - } - } - - /** - * Call the getMessageHash method of the Safe CompatibilityFallbackHandler contract - * @param messageHash The hash of the message to be signed - * @returns Returns the hash of a message to be signed by owners - * @link https://github.com/safe-global/safe-contracts/blob/8ffae95faa815acf86ec8b50021ebe9f96abde10/contracts/handler/CompatibilityFallbackHandler.sol#L26-L28 - */ - getMessageHash = async (messageHash: string): Promise => { - const safeAddress = this.#safeContract?.getAddress() || '' - - const data = this.#fallbackHandler?.encode('getMessageHash', [messageHash]) - - const safeMessageHash = await this.#ethAdapter.call({ - from: safeAddress, - to: safeAddress, - data: data || '0x' - }) - - return safeMessageHash - } - - /** - * Helper to concatenate signatures in order to validate them - * @param signatures An array of SafeSignature - * @returns The concatenated signatures - */ - buildSignature(signatures: SafeSignature[]): string { - return buildSignatureBytes(signatures) - } - - /** - * Helper function to generate a signature for a message using the EIP191 standard - * @param messageHash The hash of the message to be signed - * @returns The signature of the message - */ - async signEIP191Message(messageHash: string): Promise { - const signature = await generateSignature(this.#ethAdapter, messageHash) - - return signature - } - - /** - * Helper function to generate a Smart contract signature for a message - * @param messageHash This is the Safe message hash - * @returns A signature of the message (Smart contract signature) - */ - async signSafeMessageHash(messageHash: string): Promise { - const signature = await generateSignature(this.#ethAdapter, messageHash) - const safeAddress = this.#safeContract?.getAddress() || '' - - return new EthSafeSignature(safeAddress, signature.data, true) - } - - /** - * Helper function to generate a signature for a message using the EIP712 standard - * @param safeEIP712Args The arguments to generate the EIP712 signature - * @param methodVersion The version of the EIP712 signature - * @returns The signature of the message - */ - async signEIP712Message( - safeEIP712Args: SafeEIP712Args, - methodVersion?: 'v3' | 'v4' - ): Promise { - const signature = await generateEIP712Signature(this.#ethAdapter, safeEIP712Args, methodVersion) - - return signature - } - - parseSignature(signatures: any, safeTxHash: string, ignoreTrailing = true): string[] { - if (!signatures) { - return [] - } - - // Convert signatures to buffer if it is a string - if (typeof signatures === 'string') { - signatures = ethers.utils.arrayify(signatures) - } - - const signatureSize = 65 - const dataPosition = signatures.length - - const safeSignatures = [] - - for (let i = 0; i < signatures.length; i += signatureSize) { - if (i >= dataPosition) { - break - } - - const signature = signatures.slice(i, i + signatureSize) - - if (ignoreTrailing && signature.length < 65) { - break - } - - const v = signature[64] - const r = '0x' + ethers.utils.hexlify(signature.slice(0, 32)) - const s = '0x' + ethers.utils.hexlify(signature.slice(32, 64)) - - let safeSignature - - // Your existing logic for creating signature objects would go here - if (v === 0) { - // Contract signature - // Convert to BigInt manually - const contractSignatureLength = BigInt( - '0x' + ethers.utils.hexlify(signatures.slice(s, s + 8)) - ) - const contractSignature = ethers.utils - .hexlify(signatures.slice(s + 32, s + 32 + Number(contractSignatureLength))) - .substring(2) - safeSignature = 'Contract Signature' - } else if (v === 1) { - // Approved hash - safeSignature = 'Approved Hash' - } else if (v > 30) { - // ETH_SIGN - safeSignature = 'ETH_SIGN' - } else { - // EOA - safeSignature = 'EOA' - } - - safeSignatures.push(safeSignature) - } - - return safeSignatures - } -} - -export default SignatureManager diff --git a/packages/protocol-kit/src/utils/signatures/utils.ts b/packages/protocol-kit/src/utils/signatures/utils.ts index 9c5e9dbd6..7c3082d9d 100644 --- a/packages/protocol-kit/src/utils/signatures/utils.ts +++ b/packages/protocol-kit/src/utils/signatures/utils.ts @@ -132,7 +132,7 @@ export async function generateEIP712Signature( return new EthSafeSignature(signerAddress, signature, true) } -export const buildSignatureBytes = (signatures: SafeSignature[]): string => { +export const buildSignature = (signatures: SafeSignature[]): string => { const SIGNATURE_LENGTH_BYTES = 65 signatures.sort((left, right) => diff --git a/packages/protocol-kit/src/utils/transactions/SafeTransaction.ts b/packages/protocol-kit/src/utils/transactions/SafeTransaction.ts index d19f6ec27..489285bea 100644 --- a/packages/protocol-kit/src/utils/transactions/SafeTransaction.ts +++ b/packages/protocol-kit/src/utils/transactions/SafeTransaction.ts @@ -3,7 +3,7 @@ import { SafeTransaction, SafeTransactionData } from '@safe-global/safe-core-sdk-types' -import { buildSignatureBytes } from '../signatures' +import { buildSignature } from '../signatures' class EthSafeTransaction implements SafeTransaction { data: SafeTransactionData @@ -18,7 +18,7 @@ class EthSafeTransaction implements SafeTransaction { } encodedSignatures(): string { - return buildSignatureBytes(Array.from(this.signatures.values())) + return buildSignature(Array.from(this.signatures.values())) } } diff --git a/packages/protocol-kit/tests/e2e/eip1271.test.ts b/packages/protocol-kit/tests/e2e/eip1271.test.ts index dddfd265b..e95af0219 100644 --- a/packages/protocol-kit/tests/e2e/eip1271.test.ts +++ b/packages/protocol-kit/tests/e2e/eip1271.test.ts @@ -11,6 +11,7 @@ import { getAccounts } from './utils/setupTestNetwork' import { waitSafeTxReceipt } from './utils/transactions' import { itif } from './utils/helpers' import { BigNumber, ethers } from 'ethers' +import { buildSignature } from '@safe-global/protocol-kit/utils' chai.use(chaiAsPromised) @@ -37,7 +38,7 @@ export const EIP712_SAFE_MESSAGE_TYPE = { const MESSAGE = 'I am the owner of this Safe account' -describe('EIP1271', () => { +describe.only('EIP1271', () => { describe('Using a 2/3 Safe in the context of the EIP1271', async () => { const setupTests = deployments.createFixture(async ({ deployments }) => { await deployments.fixture() @@ -125,13 +126,10 @@ describe('EIP1271', () => { await waitSafeTxReceipt(execResponse) - const validatedResponse1 = await safeSdk1.signatures.isValidSignature(hashMessage(MESSAGE)) + const validatedResponse1 = await safeSdk1.isValidSignature(hashMessage(MESSAGE)) chai.expect(validatedResponse1).to.be.true - const validatedResponse2 = await safeSdk1.signatures.isValidSignature( - hashMessage(MESSAGE), - '0x' - ) + const validatedResponse2 = await safeSdk1.isValidSignature(hashMessage(MESSAGE), '0x') chai.expect(validatedResponse2).to.be.true }) @@ -141,30 +139,27 @@ describe('EIP1271', () => { // Hash the message const messageHash = hashMessage(MESSAGE) // Get the Safe message hash of the hashed message - const safeMessageHash = await safeSdk1.signatures.getMessageHash(messageHash) + const safeMessageHash = await safeSdk1.getMessageHash(messageHash) // Sign the Safe message hash with the owners - const ethSignSig1 = await safeSdk1.signatures.signEIP191Message(safeMessageHash) - const ethSignSig2 = await safeSdk2.signatures.signEIP191Message(safeMessageHash) + const ethSignSig1 = await safeSdk1.signMessageHash(safeMessageHash) + const ethSignSig2 = await safeSdk2.signMessageHash(safeMessageHash) // Validate the signature sending the Safe message hash and the concatenated signatures - const isValid1 = await safeSdk1.signatures.isValidSignature( + const isValid1 = await safeSdk1.isValidSignature( messageHash, - safeSdk1.signatures.buildSignature([ethSignSig1, ethSignSig2]) + buildSignature([ethSignSig1, ethSignSig2]) ) chai.expect(isValid1).to.be.true // Validate the signature sending the Safe message hash and the array of SafeSignature - const isValid2 = await safeSdk1.signatures.isValidSignature(messageHash, [ - ethSignSig1, - ethSignSig2 - ]) + const isValid2 = await safeSdk1.isValidSignature(messageHash, [ethSignSig1, ethSignSig2]) chai.expect(isValid2).to.be.true // Validate the signature is not valid when not enough signers has signed - const isValid3 = await safeSdk1.signatures.isValidSignature(messageHash, [ethSignSig1]) + const isValid3 = await safeSdk1.isValidSignature(messageHash, [ethSignSig1]) chai.expect(isValid3).to.be.false }) @@ -172,76 +167,60 @@ describe('EIP1271', () => { itif(safeVersionDeployed >= '1.3.0')( 'should allow to validate a mix EIP191 and EIP712 signatures', async () => { - const { safeSdk1, safeSdk2, safe } = await setupTests() - - const chainId: number = await safeSdk1.getChainId() - const safeVersion = await safeSdk1.getContractVersion() + const { safeSdk1, safeSdk2 } = await setupTests() // Hash the message const messageHash = hashMessage(MESSAGE) // Get the Safe message hash of the hashed message - const safeMessageHash = await safeSdk1.signatures.getMessageHash(messageHash) + const safeMessageHash = await safeSdk1.getMessageHash(messageHash) // Sign the Safe message with the owners - const ethSignSig = await safeSdk1.signatures.signEIP191Message(safeMessageHash) - const typedDataSig = await safeSdk2.signatures.signEIP712Message({ - safeAddress: safe.address, - safeVersion, - chainId, - data: messageHash // TODO: Why the messageHash and not the safeMessageHash ? - }) + const ethSignSig = await safeSdk1.signMessageHash(safeMessageHash) + const typedDataSig = await safeSdk2.signTypedData(messageHash) // Validate the signature sending the Safe message hash and the concatenated signatures - const isValid = await safeSdk1.signatures.isValidSignature(messageHash, [ - typedDataSig, - ethSignSig - ]) + const isValid = await safeSdk1.isValidSignature(messageHash, [typedDataSig, ethSignSig]) chai.expect(isValid).to.be.true } ) - itif(safeVersionDeployed >= '1.3.0')( - 'should allow to use a Smart Contract signatures', - async () => { - const { safeSdk1, safeSdk2, safeSdk3, safe } = await setupTests() - - const chainId: number = await safeSdk1.getChainId() - const safeVersion = await safeSdk1.getContractVersion() - - // Hash the message - const messageHash = hashMessage(MESSAGE) - // Get the Safe message hash - const safeMessageHash = await safeSdk1.signatures.getMessageHash(messageHash) - - // Sign the Safe message with the owners - const ethSignSig = await safeSdk1.signatures.signEIP191Message(safeMessageHash) - const typedDataSig = await safeSdk2.signatures.signEIP712Message({ - safeAddress: safe.address, - safeVersion, - chainId, - data: messageHash // TODO: Why the messageHash and not the safeMessageHash ? - }) - - // Sign with the Smart contract - const signerSafeMessageHash = await safeSdk3.signatures.getMessageHash(messageHash) - const signerSafeSig = await safeSdk3.signatures.signSafeMessageHash(signerSafeMessageHash) - - // Validate the signature sending the Safe message hash and the concatenated signatures - const isValid = await safeSdk1.signatures.isValidSignature(messageHash, [ - signerSafeSig, - ethSignSig, - typedDataSig - ]) - - chai.expect(isValid).to.be.true - } - ) + describe.only('focus', () => { + itif(safeVersionDeployed >= '1.3.0')( + 'should allow to use a Smart Contract signatures', + async () => { + const { safeSdk1, safeSdk2, safeSdk3 } = await setupTests() + + // Hash the message + const messageHash = hashMessage(MESSAGE) + // Get the Safe message hash + const safeMessageHash = await safeSdk1.getMessageHash(messageHash) + + // Sign the Safe message with the owners + const ethSignSig = await safeSdk1.signMessageHash(safeMessageHash) + console.log('signer', await safeSdk1.getEthAdapter().getSignerAddress()) + const typedDataSig = await safeSdk2.signTypedData(messageHash) + + // Sign with the Smart contract + const signerSafeMessageHash = await safeSdk3.getMessageHash(messageHash) + const signerSafeSig = await safeSdk3.signMessageHash(signerSafeMessageHash, true) + + // Validate the signature sending the Safe message hash and the concatenated signatures + const isValid = await safeSdk1.isValidSignature(messageHash, [ + signerSafeSig, + ethSignSig, + typedDataSig + ]) + + chai.expect(isValid).to.be.true + } + ) + }) itif(safeVersionDeployed >= '1.3.0')('should revert when message is not signed', async () => { const { safeSdk1 } = await setupTests() - const response = await safeSdk1.signatures.isValidSignature(hashMessage(MESSAGE), '0x') + const response = await safeSdk1.isValidSignature(hashMessage(MESSAGE), '0x') chai.expect(response).to.be.false }) @@ -252,7 +231,7 @@ describe('EIP1271', () => { const { safe, safeSdk1 } = await setupTests() const chainId = await safeSdk1.getChainId() - const safeMessageHash = await safeSdk1.signatures.getMessageHash(hashMessage(MESSAGE)) + const safeMessageHash = await safeSdk1.getMessageHash(hashMessage(MESSAGE)) chai .expect(safeMessageHash) @@ -260,7 +239,7 @@ describe('EIP1271', () => { } ) - it.only('should allow to use a Smart Contract signatures', async () => { + it.skip('should allow to use a sign transactions using smart contracts', async () => { const { safe, accounts, safeSdk1, safeSdk2 } = await setupTests() const [account1, account2] = accounts