Skip to content

Commit 11a3c33

Browse files
Merge pull request #618 from JeneaVranceanu/fix/eip-712
fix: EIP712 - allow arbitrary EIP712Hashable data models
2 parents dce0f36 + 5034e94 commit 11a3c33

File tree

3 files changed

+140
-161
lines changed

3 files changed

+140
-161
lines changed
Lines changed: 122 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,8 @@
1-
import BigInt
21
import CryptoSwift
32
import Foundation
3+
import BigInt
44
import Core
55

6-
// TODO: Refactor me
7-
8-
struct EIP712Domain: EIP712DomainHashable {
9-
let chainId: EIP712.UInt256?
10-
let verifyingContract: EIP712.Address
11-
}
12-
13-
protocol EIP712DomainHashable: EIP712Hashable {}
14-
15-
public struct SafeTx: EIP712Hashable {
16-
let to: EIP712.Address
17-
let value: EIP712.UInt256
18-
let data: EIP712.Bytes
19-
let operation: EIP712.UInt8
20-
let safeTxGas: EIP712.UInt256
21-
let baseGas: EIP712.UInt256
22-
let gasPrice: EIP712.UInt256
23-
let gasToken: EIP712.Address
24-
let refundReceiver: EIP712.Address
25-
let nonce: EIP712.UInt256
26-
27-
public init(to: EIP712.Address, value: EIP712.UInt256, data: EIP712.Bytes, operation: EIP712.UInt8, safeTxGas: EIP712.UInt256, baseGas: EIP712.UInt256, gasPrice: EIP712.UInt256, gasToken: EIP712.Address, refundReceiver: EIP712.Address, nonce: EIP712.UInt256) {
28-
self.to = to
29-
self.value = value
30-
self.data = data
31-
self.operation = operation
32-
self.safeTxGas = safeTxGas
33-
self.baseGas = baseGas
34-
self.gasPrice = gasPrice
35-
self.gasToken = gasToken
36-
self.refundReceiver = refundReceiver
37-
self.nonce = nonce
38-
}
39-
40-
}
41-
426
/// Protocol defines EIP712 struct encoding
437
public protocol EIP712Hashable {
448
var typehash: Data { get }
@@ -52,27 +16,90 @@ public class EIP712 {
5216
public typealias Bytes = Data
5317
}
5418

19+
public struct EIP712Domain: EIP712Hashable {
20+
public let chainId: EIP712.UInt256?
21+
public let verifyingContract: EIP712.Address
22+
public init(chainId: EIP712.UInt256?, verifyingContract: EIP712.Address) {
23+
self.chainId = chainId
24+
self.verifyingContract = verifyingContract
25+
}
26+
}
27+
5528
public extension EIP712.Address {
5629
static var zero: Self {
5730
EthereumAddress(Data(count: 20))!
5831
}
5932
}
6033

34+
// MARK: - Default implementation for EIP712Hashable
35+
public extension EIP712Hashable {
36+
var typehash: Data {
37+
Data(encodeType().bytes).sha3(.keccak256)
38+
}
39+
40+
func hash() throws -> Data {
41+
typealias SolidityValue = (value: Any, type: ABI.Element.ParameterType)
42+
var parameters: [Data] = [typehash]
43+
for case let (_, field) in Mirror(reflecting: self).children {
44+
let result: Data
45+
switch field {
46+
case let string as String:
47+
result = Data(string.bytes).sha3(.keccak256)
48+
case let data as EIP712.Bytes:
49+
result = data.sha3(.keccak256)
50+
case is EIP712.UInt8:
51+
result = ABIEncoder.encodeSingleType(type: .uint(bits: 8), value: field as AnyObject)!
52+
case is EIP712.UInt256:
53+
result = ABIEncoder.encodeSingleType(type: .uint(bits: 256), value: field as AnyObject)!
54+
case is EIP712.Address:
55+
result = ABIEncoder.encodeSingleType(type: .address, value: field as AnyObject)!
56+
case let hashable as EIP712Hashable:
57+
result = try hashable.hash()
58+
default:
59+
if (field as AnyObject) is NSNull {
60+
continue
61+
} else {
62+
preconditionFailure("Not solidity type")
63+
}
64+
}
65+
guard result.count == 32 else { preconditionFailure("ABI encode error") }
66+
parameters.append(result)
67+
}
68+
return Data(parameters.flatMap { $0.bytes }).sha3(.keccak256)
69+
}
70+
}
71+
72+
public func eip712encode(domainSeparator: EIP712Hashable, message: EIP712Hashable) throws -> Data {
73+
let data = try Data([UInt8(0x19), UInt8(0x01)]) + domainSeparator.hash() + message.hash()
74+
return data.sha3(.keccak256)
75+
}
76+
77+
// MARK: - Additional private and public extensions with support members
78+
6179
public extension EIP712Hashable {
62-
private var name: String {
80+
func encodeType() -> String {
81+
let dependencies = dependencies().map { $0.encodePrimaryType() }
82+
let selfPrimaryType = encodePrimaryType()
83+
84+
let result = Set(dependencies).filter { $0 != selfPrimaryType }
85+
return selfPrimaryType + result.sorted().joined()
86+
}
87+
}
88+
89+
fileprivate extension EIP712Hashable {
90+
var name: String {
6391
let fullName = "\(Self.self)"
6492
let name = fullName.components(separatedBy: ".").last ?? fullName
6593
return name
6694
}
6795

68-
private func dependencies() -> [EIP712Hashable] {
69-
let dependencies = Mirror(reflecting: self).children
96+
func dependencies() -> [EIP712Hashable] {
97+
Mirror(reflecting: self).children
7098
.compactMap { $0.value as? EIP712Hashable }
7199
.flatMap { [$0] + $0.dependencies() }
72-
return dependencies
73100
}
74101

75-
private func encodePrimaryType() -> String {
102+
func encodePrimaryType() -> String {
76103
let parametrs: [String] = Mirror(reflecting: self).children.compactMap { key, value in
77104
guard let key = key else { return nil }
78105

@@ -100,71 +127,62 @@ public extension EIP712Hashable {
100127
}
101128
return typeName + " " + key
102129
}
103-
return self.name + "(" + parametrs.joined(separator: ",") + ")"
104-
}
105-
106-
func encodeType() -> String {
107-
let dependencies = self.dependencies().map { $0.encodePrimaryType() }
108-
let selfPrimaryType = self.encodePrimaryType()
109-
110-
let result = Set(dependencies).filter { $0 != selfPrimaryType }
111-
return selfPrimaryType + result.sorted().joined()
112-
}
113-
114-
// MARK: - Default implementation
115-
116-
var typehash: Data {
117-
keccak256(encodeType())
118-
}
119-
120-
func hash() throws -> Data {
121-
typealias SolidityValue = (value: Any, type: ABI.Element.ParameterType)
122-
var parametrs: [Data] = [self.typehash]
123-
for case let (_, field) in Mirror(reflecting: self).children {
124-
let result: Data
125-
switch field {
126-
case let string as String:
127-
result = keccak256(string)
128-
case let data as EIP712.Bytes:
129-
result = keccak256(data)
130-
case is EIP712.UInt8:
131-
result = ABIEncoder.encodeSingleType(type: .uint(bits: 8), value: field as AnyObject)!
132-
case is EIP712.UInt256:
133-
result = ABIEncoder.encodeSingleType(type: .uint(bits: 256), value: field as AnyObject)!
134-
case is EIP712.Address:
135-
result = ABIEncoder.encodeSingleType(type: .address, value: field as AnyObject)!
136-
case let hashable as EIP712Hashable:
137-
result = try hashable.hash()
138-
default:
139-
if (field as AnyObject) is NSNull {
140-
continue
141-
} else {
142-
preconditionFailure("Not solidity type")
143-
}
144-
}
145-
guard result.count == 32 else { preconditionFailure("ABI encode error") }
146-
parametrs.append(result)
147-
}
148-
let encoded = parametrs.flatMap { $0.bytes }
149-
return keccak256(encoded)
130+
return name + "(" + parametrs.joined(separator: ",") + ")"
150131
}
151132
}
152133

153-
// Encode functions
154-
func eip712encode(domainSeparator: EIP712Hashable, message: EIP712Hashable) throws -> Data {
155-
let data = try Data([UInt8(0x19), UInt8(0x01)]) + domainSeparator.hash() + message.hash()
156-
return keccak256(data)
157-
}
158-
159-
// MARK: - keccak256
160-
private func keccak256(_ data: [UInt8]) -> Data {
161-
Data(SHA3(variant: .keccak256).calculate(for: data))
162-
}
134+
// MARK: - Gnosis Safe Transaction model
135+
136+
/// Gnosis Safe Transaction.
137+
/// https://docs.gnosis-safe.io/tutorials/tutorial_tx_service_initiate_sign
138+
///
139+
/// Note for web3swift developers: **DO NOT CHANGE THE ORDER OF VARIABLES**.
140+
///
141+
/// Changing the order will result in a different hash.
142+
/// Order must match the implementation of hash calculation in
143+
/// [`GnosisSafe.sol`](https://github.com/safe-global/safe-contracts/blob/main/contracts/GnosisSafe.sol#L126).
144+
public struct GnosisSafeTx: EIP712Hashable {
145+
/// Checksummed address
146+
let to: EIP712.Address
147+
/// Value in wei
148+
let value: EIP712.UInt256
149+
/// 0x prefixed hex string
150+
let data: EIP712.Bytes
151+
/// `0` CALL, `1` DELEGATE_CALL
152+
let operation: EIP712.UInt8
153+
/// Max gas to use in the transaction
154+
let safeTxGas: EIP712.UInt256
155+
/// Gast costs not related to the transaction execution (signature check, refund payment...)
156+
let baseGas: EIP712.UInt256
157+
/// Gas price used for the refund calculation
158+
let gasPrice: EIP712.UInt256
159+
/// Token address, **must be checksummed**, (held by the Safe) to be used as a refund to the sender, if `null` is Ether
160+
let gasToken: EIP712.Address
161+
/// Checksummed address of receiver of gas payment (or `null` if tx.origin)
162+
let refundReceiver: EIP712.Address
163+
/// Nonce of the Safe, transaction cannot be executed until Safe's nonce is not equal to this nonce
164+
let nonce: EIP712.UInt256
163165

164-
private func keccak256(_ string: String) -> Data {
165-
keccak256(Array(string.utf8))
166-
}
166+
public init(to: EIP712.Address,
167+
value: EIP712.UInt256,
168+
data: EIP712.Bytes,
169+
operation: EIP712.UInt8,
170+
safeTxGas: EIP712.UInt256,
171+
baseGas: EIP712.UInt256,
172+
gasPrice: EIP712.UInt256,
173+
gasToken: EIP712.Address,
174+
refundReceiver: EIP712.Address,
175+
nonce: EIP712.UInt256) {
176+
self.to = to
177+
self.value = value
178+
self.data = data
179+
self.operation = operation
180+
self.safeTxGas = safeTxGas
181+
self.baseGas = baseGas
182+
self.gasPrice = gasPrice
183+
self.gasToken = gasToken
184+
self.refundReceiver = refundReceiver
185+
self.nonce = nonce
186+
}
167187

168-
private func keccak256(_ data: Data) -> Data {
169-
keccak256(data.bytes)
170188
}

Sources/web3swift/Web3/Web3+Signing.swift

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Created by Alex Vlasov.
33
// Copyright © 2018 Alex Vlasov. All rights reserved.
44
//
5-
// Refactor to support EIP-2718 Enveloping by Mark Loit 2022
5+
// Refactor to support EIP-2718 Enveloping by Mark Loit 2022
66

77
import Foundation
88
import BigInt
@@ -27,26 +27,28 @@ public struct Web3Signer {
2727
var privateKey = try keystore.UNSAFE_getPrivateKeyData(password: password, account: account)
2828
defer { Data.zero(&privateKey) }
2929
guard let hash = Utilities.hashPersonalMessage(personalMessage) else { return nil }
30-
let (compressedSignature, _) = SECP256K1.signForRecovery(hash: hash, privateKey: privateKey, useExtraEntropy: useExtraEntropy)
30+
let (compressedSignature, _) = SECP256K1.signForRecovery(hash: hash,
31+
privateKey: privateKey,
32+
useExtraEntropy: useExtraEntropy)
3133
return compressedSignature
3234
}
3335

34-
public static func signEIP712(safeTx: SafeTx,
36+
public static func signEIP712(_ eip712Hashable: EIP712Hashable,
3537
keystore: BIP32Keystore,
3638
verifyingContract: EthereumAddress,
3739
account: EthereumAddress,
3840
password: String? = nil,
3941
chainId: BigUInt? = nil) throws -> Data {
4042

41-
let domainSeparator: EIP712DomainHashable = EIP712Domain(chainId: chainId, verifyingContract: verifyingContract)
42-
43-
let password = password ?? ""
44-
let hash = try eip712encode(domainSeparator: domainSeparator, message: safeTx)
45-
46-
guard let signature = try Web3Signer.signPersonalMessage(hash, keystore: keystore, account: account, password: password) else {
43+
let domainSeparator: EIP712Hashable = EIP712Domain(chainId: chainId, verifyingContract: verifyingContract)
44+
let hash = try eip712encode(domainSeparator: domainSeparator, message: eip712Hashable)
45+
guard let signature = try Web3Signer.signPersonalMessage(hash,
46+
keystore: keystore,
47+
account: account,
48+
password: password ?? "")
49+
else {
4750
throw Web3Error.dataError
4851
}
49-
5052
return signature
5153
}
5254
}

0 commit comments

Comments
 (0)