Skip to content

Commit

Permalink
Add SafeMessage and signMessage utilities
Browse files Browse the repository at this point in the history
  • Loading branch information
yagopv committed Oct 30, 2023
1 parent ee50b5a commit 43e7720
Show file tree
Hide file tree
Showing 4 changed files with 255 additions and 79 deletions.
52 changes: 51 additions & 1 deletion packages/protocol-kit/src/Safe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import {
getProxyFactoryContract,
getSafeContract
} from './contracts/safeDeploymentContracts'
import SafeMessage from './utils/messages/SafeMessage'

class Safe {
#predictedSafe?: PredictedSafeProps
Expand Down Expand Up @@ -529,6 +530,55 @@ class Safe {
return hashSafeMessage(message)
}

createMessage(message: string | EIP712TypedData): SafeMessage {
return new SafeMessage(message)
}

async signMessage(
message: SafeMessage,
signingMethod = 'eth_signTypedData_v4',
isSmartContract = false
): Promise<SafeMessage> {
const owners = await this.getOwners()
const signerAddress = await this.#ethAdapter.getSignerAddress()
if (!signerAddress) {
throw new Error('EthAdapter must be initialized with a signer to use this method')
}
const addressIsOwner = owners.some(
(owner: string) => signerAddress && sameString(owner, signerAddress)
)
if (!addressIsOwner) {
throw new Error('Transactions can only be signed by Safe owners')
}

let signature: SafeSignature
if (signingMethod === 'eth_signTypedData_v4') {
signature = await this.signTypedData(message, 'v4', isSmartContract)
} else if (signingMethod === 'eth_signTypedData_v3') {
signature = await this.signTypedData(message, 'v3', isSmartContract)
} else if (signingMethod === 'eth_signTypedData') {
signature = await this.signTypedData(message, undefined, isSmartContract)
} else {
const safeVersion = await this.getContractVersion()
if (!hasSafeFeature(SAFE_FEATURES.ETH_SIGN, safeVersion)) {
throw new Error('eth_sign is only supported by Safes >= v1.1.0')
}

const safeMessageHash = await this.getSafeMessageHash(this.hashSafeMessage(message.data))
signature = await this.signHash(safeMessageHash, isSmartContract)
}

const signedSafeMessage = this.createMessage(message.data)

message.signatures.forEach((signature: EthSafeSignature) => {
signedSafeMessage.addSignature(signature)
})

signedSafeMessage.addSignature(signature)

return signedSafeMessage
}

/**
* Signs a transaction according to the EIP-712 using the current signer account.
*
Expand All @@ -537,7 +587,7 @@ class Safe {
* @returns The Safe signature
*/
async signTypedData(
eip712Data: SafeTransaction | EIP712TypedData | string,
eip712Data: SafeTransaction | SafeMessage,
methodVersion?: 'v3' | 'v4',
isSmartContract = false
): Promise<SafeSignature> {
Expand Down
21 changes: 21 additions & 0 deletions packages/protocol-kit/src/utils/messages/SafeMessage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { EIP712TypedData, SafeMessage, SafeSignature } from '@safe-global/safe-core-sdk-types'
import { buildSignature } from '../signatures'

class EthSafeMessage implements SafeMessage {
data: EIP712TypedData | string
signatures: Map<string, SafeSignature> = new Map()

constructor(data: EIP712TypedData | string) {
this.data = data
}

addSignature(signature: SafeSignature): void {
this.signatures.set(signature.signer.toLowerCase(), signature)
}

encodedSignatures(): string {
return buildSignature(Array.from(this.signatures.values()))
}
}

export default EthSafeMessage
254 changes: 176 additions & 78 deletions packages/protocol-kit/tests/e2e/eip1271.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ethers } from 'ethers'
import Safe from '@safe-global/protocol-kit/index'
import { safeVersionDeployed } from '@safe-global/protocol-kit/hardhat/deploy/deploy-contracts'
import {
Expand All @@ -14,8 +15,8 @@ import { getEthAdapter } from './utils/setupEthAdapter'
import { getAccounts } from './utils/setupTestNetwork'
import { waitSafeTxReceipt } from './utils/transactions'
import { itif } from './utils/helpers'
import { ethers } from 'ethers'
import { buildSignature, hashSafeMessage } from '@safe-global/protocol-kit/utils'
import { buildSignature } from '@safe-global/protocol-kit/utils'
import SafeMessage from '../../src/utils/messages/SafeMessage'

chai.use(chaiAsPromised)

Expand Down Expand Up @@ -165,109 +166,206 @@ describe.only('EIP1271', () => {
}
)

itif(safeVersionDeployed >= '1.3.0')('should validate off-chain messages', async () => {
const { safeSdk1, safeSdk2 } = await setupTests()
itif(safeVersionDeployed >= '1.3.0')('should revert when message is not signed', async () => {
const { safeSdk1 } = await setupTests()

// Hash the message
const messageHash = await safeSdk1.hashSafeMessage(MESSAGE)
const safeMessageHash = await safeSdk1.getSafeMessageHash(messageHash)
const response = await safeSdk1.isValidSignature(
await safeSdk1.hashSafeMessage(MESSAGE),
'0x'
)

// Sign the Safe message hash with the owners
const ethSignSig1 = await safeSdk1.signHash(safeMessageHash)
const ethSignSig2 = await safeSdk2.signHash(safeMessageHash)
chai.expect(response).to.be.false
})

// Validate the signature sending the Safe message hash and the concatenated signatures
const isValid1 = await safeSdk1.isValidSignature(
messageHash,
buildSignature([ethSignSig1, ethSignSig2])
)
describe('getSafeMessageHash(), signHash(), signTypedData()', () => {
itif(safeVersionDeployed >= '1.3.0')(
'should allow to validate off-chain messages',
async () => {
const { safeSdk1, safeSdk2 } = await setupTests()

chai.expect(isValid1).to.be.true
// Hash the message
const messageHash = await safeSdk1.hashSafeMessage(MESSAGE)
const safeMessageHash = await safeSdk1.getSafeMessageHash(messageHash)

// Validate the signature sending the Safe message hash and the array of SafeSignature
const isValid2 = await safeSdk1.isValidSignature(messageHash, [ethSignSig1, ethSignSig2])
// Sign the Safe message hash with the owners
const ethSignSig1 = await safeSdk1.signHash(safeMessageHash)
const ethSignSig2 = await safeSdk2.signHash(safeMessageHash)

chai.expect(isValid2).to.be.true
// Validate the signature sending the Safe message hash and the concatenated signatures
const isValid1 = await safeSdk1.isValidSignature(
messageHash,
buildSignature([ethSignSig1, ethSignSig2])
)

// Validate the signature is not valid when not enough signers has signed
const isValid3 = await safeSdk1.isValidSignature(messageHash, [ethSignSig1])
chai.expect(isValid1).to.be.true

chai.expect(isValid3).to.be.false
})
// Validate the signature sending the Safe message hash and the array of SafeSignature
const isValid2 = await safeSdk1.isValidSignature(messageHash, [ethSignSig1, ethSignSig2])

itif(safeVersionDeployed >= '1.3.0')(
'should validate a mix EIP191 and EIP712 signatures',
async () => {
const { safeSdk1, safeSdk2 } = await setupTests()
chai.expect(isValid2).to.be.true

// Hash the message
const messageHash = await safeSdk1.hashSafeMessage(MESSAGE)
const safeMessageHash = await safeSdk1.getSafeMessageHash(messageHash)
// Validate the signature is not valid when not enough signers has signed
const isValid3 = await safeSdk1.isValidSignature(messageHash, [ethSignSig1])

// Sign the Safe message with the owners
const ethSignSig = await safeSdk1.signHash(safeMessageHash)
chai.expect(isValid3).to.be.false
}
)

const typedDataSig = await safeSdk2.signTypedData(MESSAGE)
// Validate the signature sending the Safe message hash and the concatenated signatures
const isValid = await safeSdk1.isValidSignature(messageHash, [typedDataSig, ethSignSig])
itif(safeVersionDeployed >= '1.3.0')(
'should validate a mix EIP191 and EIP712 signatures',
async () => {
const { safeSdk1, safeSdk2 } = await setupTests()

chai.expect(isValid).to.be.true
}
)
// Hash the message
const messageHash = await safeSdk1.hashSafeMessage(MESSAGE)
const safeMessageHash = await safeSdk1.getSafeMessageHash(messageHash)

itif(safeVersionDeployed >= '1.3.0')(
'should validate Smart contracts as signers (threshold = 1)',
async () => {
const { safeSdk1, safeSdk2, safeSdk3 } = await setupTests()
// Sign the Safe message with the owners
const ethSignSig = await safeSdk1.signHash(safeMessageHash)

// Hash the message
const messageHash = await safeSdk1.hashSafeMessage(MESSAGE)
const safeMessageHash = await safeSdk1.getSafeMessageHash(messageHash)
const typedDataSig = await safeSdk2.signTypedData(MESSAGE)
// Validate the signature sending the Safe message hash and the concatenated signatures
const isValid = await safeSdk1.isValidSignature(messageHash, [typedDataSig, ethSignSig])

// Sign the Safe message with the owners
const ethSignSig = await safeSdk1.signHash(safeMessageHash)
const typedDataSig = await safeSdk2.signTypedData(messageHash)
chai.expect(isValid).to.be.true
}
)

// Sign with the Smart contract
const safeSignerMessageHash = await safeSdk3.getSafeMessageHash(messageHash)
const signerSafeSig = await safeSdk3.signHash(safeSignerMessageHash, true)
itif(safeVersionDeployed >= '1.3.0')(
'should validate Smart contracts as signers (threshold = 1)',
async () => {
const { safeSdk1, safeSdk2, safeSdk3 } = await setupTests()

// Validate the signature sending the Safe message hash and the concatenated signatures
const isValid = await safeSdk1.isValidSignature(messageHash, [
signerSafeSig,
ethSignSig,
typedDataSig
])
// Hash the message
const messageHash = await safeSdk1.hashSafeMessage(MESSAGE)
const safeMessageHash = await safeSdk1.getSafeMessageHash(messageHash)

chai.expect(isValid).to.be.true
}
)
// Sign the Safe message with the owners
const ethSignSig = await safeSdk1.signHash(safeMessageHash)
const typedDataSig = await safeSdk2.signTypedData(messageHash)

itif(safeVersionDeployed >= '1.3.0')('should revert when message is not signed', async () => {
const { safeSdk1 } = await setupTests()
// Sign with the Smart contract
const safeSignerMessageHash = await safeSdk3.getSafeMessageHash(messageHash)
const signerSafeSig = await safeSdk3.signHash(safeSignerMessageHash, true)

const response = await safeSdk1.isValidSignature(
await safeSdk1.hashSafeMessage(MESSAGE),
'0x'
// 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
}
)

chai.expect(response).to.be.false
itif(safeVersionDeployed >= '1.3.0')(
'should generate the correct safeMessageHash',
async () => {
const { safeAddress, safeSdk1 } = await setupTests()

const chainId = await safeSdk1.getChainId()
const messageHash = await safeSdk1.hashSafeMessage(MESSAGE)
const safeMessageHash = await safeSdk1.getSafeMessageHash(messageHash)

chai
.expect(safeMessageHash)
.to.be.eq(calculateSafeMessageHash(safeAddress, messageHash, chainId))
}
)
})

itif(safeVersionDeployed >= '1.3.0')(
'should generate the correct safeMessageHash',
async () => {
const { safeAddress, safeSdk1 } = await setupTests()
describe('signMessage()', () => {
itif(safeVersionDeployed >= '1.3.0')('should validate off-chain messages', async () => {
const { safeSdk1, safeSdk2 } = await setupTests()

const chainId = await safeSdk1.getChainId()
const messageHash = await safeSdk1.hashSafeMessage(MESSAGE)
const safeMessageHash = await safeSdk1.getSafeMessageHash(messageHash)
// EIP191 sign the Safe message with owners
const safeMessage = safeSdk1.createMessage(MESSAGE)

const signedMessage1: SafeMessage = await safeSdk1.signMessage(safeMessage, 'eth_sign')
const signedMessage2: SafeMessage = await safeSdk2.signMessage(signedMessage1, 'eth_sign')

// Validate the signature
chai.expect(
await safeSdk1.isValidSignature(
safeSdk1.hashSafeMessage(MESSAGE),
signedMessage2.encodedSignatures()
)
).to.be.true

// Validate the signature is not valid when not enough signers has signed
chai.expect(
await safeSdk1.isValidSignature(
safeSdk1.hashSafeMessage(MESSAGE),
signedMessage1.encodedSignatures()
)
).to.be.false
})

chai
.expect(safeMessageHash)
.to.be.eq(calculateSafeMessageHash(safeAddress, messageHash, chainId))
}
)
itif(safeVersionDeployed >= '1.3.0')(
'should validate a mix EIP191 and EIP712 signatures',
async () => {
const { safeSdk1, safeSdk2 } = await setupTests()

// EIP191 and EIP712 sign the Safe message with owners
const safeMessage = safeSdk1.createMessage(MESSAGE)

const signedMessage1: SafeMessage = await safeSdk1.signMessage(safeMessage, 'eth_sign')
const signedMessage2: SafeMessage = await safeSdk2.signMessage(
signedMessage1,
'eth_signTypedData_v4'
)

// Validate the signature
chai.expect(
await safeSdk1.isValidSignature(
safeSdk1.hashSafeMessage(MESSAGE),
signedMessage2.encodedSignatures()
)
).to.be.true
}
)

itif(safeVersionDeployed >= '1.3.0')(
'should validate Smart contracts as signers (threshold = 1)',
async () => {
const { safeSdk1, safeSdk3 } = await setupTests()

// Sign the Safe message with owners
const safeMessage = safeSdk1.createMessage(MESSAGE)

const signedMessage1: SafeMessage = await safeSdk1.signMessage(safeMessage, 'eth_sign')
const signedMessage2: SafeMessage = await safeSdk3.signMessage(
signedMessage1,
'eth_sign',
true
)

// Validate the signature
chai.expect(
await safeSdk1.isValidSignature(
safeSdk1.hashSafeMessage(MESSAGE),
signedMessage2.encodedSignatures()
)
).to.be.true
}
)

itif(safeVersionDeployed >= '1.3.0')(
'should generate the correct safeMessageHash',
async () => {
const { safeAddress, safeSdk1 } = await setupTests()

const chainId = await safeSdk1.getChainId()
const messageHash = safeSdk1.hashSafeMessage(MESSAGE)
const safeMessageHash = await safeSdk1.getSafeMessageHash(messageHash)

chai
.expect(safeMessageHash)
.to.be.eq(calculateSafeMessageHash(safeAddress, messageHash, chainId))
}
)
})

it.skip('should allow use to sign transactions using Safe Accounts (threshold = 1)', async () => {
const { signerSafeAddress, safeAddress, accounts, safeSdk1, safeSdk2, safeSdk3, signerSafe } =
Expand Down
Loading

0 comments on commit 43e7720

Please sign in to comment.