Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(protocol-kit): EIP-1271 integration #529

Closed
wants to merge 51 commits into from
Closed
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
c5e3cfd
isValidSignature implementation
yagopv Sep 7, 2023
b2c15da
Add test for on chain message
yagopv Sep 11, 2023
329b876
Refactor
yagopv Sep 12, 2023
9495156
Mini fix
yagopv Sep 12, 2023
d351f95
Add invalid validation test and trycatch
yagopv Sep 12, 2023
878c3cc
Remove only's
yagopv Sep 12, 2023
3f742b1
Fix import
yagopv Sep 12, 2023
2c76b38
Test for 1.3.0 and greater
yagopv Sep 12, 2023
edc090f
Merge branch 'release' of https://github.com/safe-global/safe-core-sd…
yagopv Sep 14, 2023
f32813f
Add test for offchain signatures
yagopv Sep 20, 2023
2851fba
Simplify test
yagopv Sep 20, 2023
53b9713
We use only a isValidSignature call as it includes the other
yagopv Sep 25, 2023
1bc9c61
Use contract getMessageHash
yagopv Sep 26, 2023
f58ced4
Use contract getMessageHash
yagopv Sep 26, 2023
fb61af6
Merge branch 'feat/validate-signatures' of https://github.com/safe-gl…
yagopv Sep 26, 2023
5ac72ef
Some changes
yagopv Sep 26, 2023
72b99aa
Rethink signatures
yagopv Sep 26, 2023
c169959
Use this.signatures
yagopv Sep 26, 2023
19ef967
Use DEFAULT_SAFE_VERSION
yagopv Sep 27, 2023
a42a498
Rename tests file
yagopv Sep 27, 2023
2ec38ea
Improve tests
yagopv Sep 27, 2023
03b72bf
Signature manager preview
yagopv Sep 27, 2023
643e270
Improve tests
yagopv Sep 27, 2023
e4e3dd8
Add support for EIP712 signatures in the signature manager
yagopv Sep 28, 2023
85ea541
Fix typo
yagopv Sep 28, 2023
e4238bf
Add SmartContractSignature support
yagopv Sep 28, 2023
5acf0b8
Add smart contract tests
yagopv Sep 28, 2023
14e9636
Complete testing
yagopv Sep 28, 2023
c46cb69
Delete console.logs
yagopv Sep 28, 2023
d2bfa4e
Remove only
yagopv Sep 28, 2023
bbfe4c9
Remove some console.logs
yagopv Sep 28, 2023
0f10ad2
Add playground
yagopv Oct 2, 2023
de1d0c9
Add support for Smart Contract Signatures in transactions
yagopv Oct 4, 2023
4b0fade
Add transaction signing
yagopv Oct 6, 2023
9931808
Refactor
yagopv Oct 6, 2023
0c175b6
Simplify
yagopv Oct 6, 2023
311f9ee
Simplify
yagopv Oct 6, 2023
a1918a3
Basic tx testing
yagopv Oct 6, 2023
d133edb
Add transaction test and remove isSmartContract detection
yagopv Oct 9, 2023
6c89173
Update test
yagopv Oct 9, 2023
bcc2354
Refactor api
yagopv Oct 9, 2023
eb92a4a
Rename getTransactionHash and signTransactionHash
yagopv Oct 10, 2023
6ebfb00
Refactor Safe
yagopv Oct 10, 2023
388184d
REmove unused fn
yagopv Oct 10, 2023
179f1e8
Update playground
yagopv Oct 10, 2023
c338d7c
rename param
yagopv Oct 10, 2023
cf33e69
Improve signTypedData
yagopv Oct 10, 2023
e41cd96
Generate tests for typed data
yagopv Oct 10, 2023
38a1f49
Fix test
yagopv Oct 10, 2023
e0afafe
Add isSmartContract param to signTransaction
yagopv Oct 10, 2023
994c6a9
Upload version with preimage
yagopv Oct 11, 2023
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
21 changes: 14 additions & 7 deletions packages/protocol-kit/src/Safe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ 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,
Expand All @@ -42,11 +43,7 @@ import {
isSafeMultisigTransactionResponse,
sameString
} from './utils'
import {
generateEIP712Signature,
generatePreValidatedSignature,
generateSignature
} from './utils/signatures/utils'
import { generateEIP712Signature, generatePreValidatedSignature } from './utils/signatures/utils'
import EthSafeTransaction from './utils/transactions/SafeTransaction'
import { SafeTransactionOptionalProps } from './utils/transactions/types'
import {
Expand All @@ -60,6 +57,7 @@ import {
getProxyFactoryContract,
getSafeContract
} from './contracts/safeDeploymentContracts'
import SignaturesManager from './managers/signatureManager'

class Safe {
#predictedSafe?: PredictedSafeProps
Expand All @@ -69,6 +67,7 @@ class Safe {
#moduleManager!: ModuleManager
#guardManager!: GuardManager
#fallbackHandlerManager!: FallbackHandlerManager
signatures!: SignaturesManager

/**
* Creates an instance of the Safe Core SDK.
Expand Down Expand Up @@ -122,6 +121,14 @@ class Safe {
this.#ethAdapter,
this.#contractManager.safeContract
)

this.signatures = new SignaturesManager(
this.#ethAdapter,
this.#contractManager.safeContract,
contractNetworks
)

await this.signatures.init()
}

/**
Expand Down Expand Up @@ -503,7 +510,7 @@ class Safe {
* @returns The Safe signature
*/
async signTransactionHash(hash: string): Promise<SafeSignature> {
return generateSignature(this.#ethAdapter, hash)
return this.signatures.signEIP191Message(hash)
}

/**
Expand All @@ -523,7 +530,7 @@ class Safe {
chainId: await this.getEthAdapter().getChainId(),
safeTransactionData: safeTransaction.data
}
return generateEIP712Signature(this.#ethAdapter, safeTransactionEIP712Args, methodVersion)
return this.signatures.signEIP712Message(safeTransactionEIP712Args, methodVersion)
}

/**
Expand Down
127 changes: 127 additions & 0 deletions packages/protocol-kit/src/managers/signatureManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import {
CompatibilityFallbackHandlerContract,
EthAdapter,
SafeContract,
SafeSignature,
SafeTransactionEIP712Args
} from '@safe-global/safe-core-sdk-types'
import { getCompatibilityFallbackHandlerContract } from '../contracts/safeDeploymentContracts'
import { ContractNetworksConfig } from '../types'
import { generateSignature, generateEIP712Signature } from '../utils'

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
}

async init() {
const safeVersion = (await this.#safeContract?.getVersion()) ?? '1.3.0'
yagopv marked this conversation as resolved.
Show resolved Hide resolved
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'
* @param safeSdk An instance of Safe
* @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: string): Promise<boolean> => {
const safeAddress = this.#safeContract?.getAddress() || ''

const eip1271data =
this.#fallbackHandler?.encode('isValidSignature(bytes32,bytes)', [messageHash, signature]) ||
'0x'

try {
const isValidSignatureResponse = await this.#ethAdapter.call({
from: safeAddress,
to: safeAddress,
data: eip1271data
})

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
* @param safeSdk An instance of Safe
* @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<string> => {
const safeAddress = this.#safeContract?.getAddress() || ''

const data = this.#fallbackHandler?.encode('getMessageHash', [messageHash]) || '0x'

const safeMessageHash = await this.#ethAdapter.call({
from: safeAddress,
to: safeAddress,
data
})

return safeMessageHash
}

buildSignature(signatures: SafeSignature[]): string {
signatures.sort((left, right) =>
left.signer.toLowerCase().localeCompare(right.signer.toLowerCase())
)

let signatureBytes = '0x'

for (const sig of signatures) {
signatureBytes += sig.data.slice(2)
}

return signatureBytes
}

async signEIP191Message(hash: string): Promise<SafeSignature> {
const signature = await generateSignature(this.#ethAdapter, hash)

return signature
}

async signEIP712Message(
safeTransactionEIP712Args: SafeTransactionEIP712Args,
methodVersion?: 'v3' | 'v4'
): Promise<SafeSignature> {
const signature = await generateEIP712Signature(
this.#ethAdapter,
safeTransactionEIP712Args,
methodVersion
)

return signature
}
}

export default SignatureManager
184 changes: 184 additions & 0 deletions packages/protocol-kit/tests/e2e/isValidSignature.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import Safe, { EthSafeSignature } from '@safe-global/protocol-kit/index'
import { safeVersionDeployed } from '@safe-global/protocol-kit/hardhat/deploy/deploy-contracts'
import {
OperationType,
SafeSignature,
SafeTransactionDataPartial
} from '@safe-global/safe-core-sdk-types'
import chai from 'chai'
import chaiAsPromised from 'chai-as-promised'
import { deployments, waffle } from 'hardhat'
import { getContractNetworks } from './utils/setupContractNetworks'
import { getSafeWithOwners } from './utils/setupContracts'
import { getEthAdapter } from './utils/setupEthAdapter'
import { getAccounts } from './utils/setupTestNetwork'
import { waitSafeTxReceipt } from './utils/transactions'
import { soliditySha3, utf8ToHex } from 'web3-utils'
import { itif } from './utils/helpers'
import { ethers } from 'ethers'

chai.use(chaiAsPromised)

export const calculateSafeMessageHash = (
safeAddress: string,
message: string,
chainId: number
): string => {
return ethers.utils._TypedDataEncoder.hash(
{ verifyingContract: safeAddress, chainId },
EIP712_SAFE_MESSAGE_TYPE,
{ message }
)
}

const hashMessage = (message: string): string => {
return soliditySha3(utf8ToHex(message)) || ''
}

export const EIP712_SAFE_MESSAGE_TYPE = {
// "SafeMessage(bytes message)"
SafeMessage: [{ type: 'bytes', name: 'message' }]
}

const MESSAGE = 'I am the owner of this Safe account'

describe.only('isValidSignature', async () => {
const setupTests = deployments.createFixture(async ({ deployments }) => {
await deployments.fixture()
const accounts = await getAccounts()
const chainId: number = (await waffle.provider.getNetwork()).chainId
const contractNetworks = await getContractNetworks(chainId)

return {
safe: await getSafeWithOwners([accounts[0].address, accounts[1].address]),
accounts,
contractNetworks,
chainId
}
})

itif(safeVersionDeployed >= '1.3.0')(
'should validate signed messages (Safe 2 owners, threshold 2)',
async () => {
const { accounts, contractNetworks, safe } = await setupTests()
const [account1, account2] = accounts

const ethAdapter1 = await getEthAdapter(account1.signer)
const safeSdk1 = await Safe.create({
ethAdapter: ethAdapter1,
safeAddress: safe.address,
contractNetworks
})

const ethAdapter2 = await getEthAdapter(account2.signer)
const safeSdk2 = await Safe.create({
ethAdapter: ethAdapter2,
safeAddress: safe.address,
contractNetworks
})

const chainId: number = await safeSdk1.getChainId()

const customContract = contractNetworks[chainId]

const signMessageLibContract = await ethAdapter1.getSignMessageLibContract({
safeVersion: await safeSdk1.getContractVersion(),
customContractAddress: customContract.signMessageLibAddress,
customContractAbi: customContract.signMessageLibAbi
})

const txData = signMessageLibContract.encode('signMessage', [hashMessage(MESSAGE)])

const safeTransactionData: SafeTransactionDataPartial = {
to: customContract.signMessageLibAddress,
value: '0',
data: txData,
operation: OperationType.DelegateCall
}

const tx = await safeSdk1.createTransaction({ safeTransactionData })
const signedTx = await safeSdk1.signTransaction(tx)
const signedTx2 = await safeSdk2.signTransaction(signedTx)
const txResponse = await safeSdk1.executeTransaction(signedTx2)

await waitSafeTxReceipt(txResponse)

const txResponse2 = await safeSdk1.signatures.isValidSignature(hashMessage(MESSAGE), '0x')

chai.expect(txResponse2).to.be.true
}
)

itif(safeVersionDeployed >= '1.3.0')('should revert if message is not signed', async () => {
const { accounts, contractNetworks } = await setupTests()
const [account1] = accounts
const safe = await getSafeWithOwners([account1.address])
const ethAdapter1 = await getEthAdapter(account1.signer)
const safeSdk1 = await Safe.create({
ethAdapter: ethAdapter1,
safeAddress: safe.address,
contractNetworks
})

const response = await safeSdk1.signatures.isValidSignature(hashMessage(MESSAGE), '0x')

chai.expect(response).to.be.false
})

itif(safeVersionDeployed >= '1.3.0')('should generate the correct hash', async () => {
const { safe, accounts, contractNetworks } = await setupTests()
const [account1] = accounts
const ethAdapter = await getEthAdapter(account1.signer)
const safeSdk = await Safe.create({
ethAdapter: ethAdapter,
safeAddress: safe.address,
contractNetworks
})

const chainId = await safeSdk.getChainId()
const safeMessageHash = await safeSdk.signatures.getMessageHash(hashMessage(MESSAGE))

chai
.expect(safeMessageHash)
.to.be.eq(calculateSafeMessageHash(safe.address, hashMessage(MESSAGE), chainId))
})

itif(safeVersionDeployed >= '1.3.0')(
'should validate off chain signatures (Safe 2 owners, threshold 2)',
async () => {
const { accounts, contractNetworks, safe } = await setupTests()
const [account1, account2] = accounts
const ethAdapter = await getEthAdapter(account1.signer)
const ethAdapter2 = await getEthAdapter(account2.signer)

const safeSdk1 = await Safe.create({
ethAdapter: ethAdapter,
safeAddress: safe.address,
contractNetworks
})

const safeSdk2 = await Safe.create({
ethAdapter: ethAdapter2,
safeAddress: safe.address,
contractNetworks
})

// Hash the message
const messageHash = hashMessage(MESSAGE)
// Get the Safe message hash of the hashed message
const safeMessageHash = await safeSdk1.signatures.getMessageHash(messageHash)

// Sign the Safe message hash with the owners
const ethSignSig1 = await safeSdk1.signatures.signEIP191Message(safeMessageHash)
const ethSignSig2 = await safeSdk2.signatures.signEIP191Message(safeMessageHash)

// Validate the signature
const isValid = await safeSdk1.signatures.isValidSignature(
messageHash,
safeSdk1.signatures.buildSignature([ethSignSig1, ethSignSig2])
)

chai.expect(isValid).to.be.true
}
)
})