Skip to content

Commit 39520ec

Browse files
Merge pull request #396 from skywinder/pr381-review
add support of EIP-712 signature
2 parents 4ad1217 + e7ff234 commit 39520ec

File tree

4 files changed

+346
-0
lines changed

4 files changed

+346
-0
lines changed

Sources/web3swift/Transaction/TransactionSigner.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,25 @@ public struct Web3Signer {
111111
}
112112
}
113113

114+
public static func signEIP712(safeTx: SafeTx,
115+
keystore: BIP32Keystore,
116+
verifyingContract: EthereumAddress,
117+
account: EthereumAddress,
118+
password: String? = nil,
119+
chainId: BigUInt? = nil) throws -> Data {
120+
121+
let domainSeparator: EIP712DomainHashable = EIP712Domain(chainId: chainId, verifyingContract: verifyingContract)
122+
123+
let password = password ?? ""
124+
let hash = try eip712encode(domainSeparator: domainSeparator, message: safeTx)
125+
126+
guard let signature = try Web3Signer.signPersonalMessage(hash, keystore: keystore, account: account, password: password) else {
127+
throw Web3Error.dataError
128+
}
129+
130+
return signature;
131+
}
132+
114133
}
115134

116135

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import BigInt
2+
import CryptoSwift
3+
import Foundation
4+
5+
struct EIP712Domain: EIP712DomainHashable {
6+
let chainId: EIP712.UInt256?
7+
let verifyingContract: EIP712.Address
8+
}
9+
10+
protocol EIP712DomainHashable: EIP712Hashable {}
11+
12+
public struct SafeTx: EIP712Hashable {
13+
let to: EIP712.Address
14+
let value: EIP712.UInt256
15+
let data: EIP712.Bytes
16+
let operation: EIP712.UInt8
17+
let safeTxGas: EIP712.UInt256
18+
let baseGas: EIP712.UInt256
19+
let gasPrice: EIP712.UInt256
20+
let gasToken: EIP712.Address
21+
let refundReceiver: EIP712.Address
22+
let nonce: EIP712.UInt256
23+
}
24+
25+
/// Protocol defines EIP712 struct encoding
26+
protocol EIP712Hashable {
27+
var typehash: Data { get }
28+
func hash() throws -> Data
29+
}
30+
31+
class EIP712 {
32+
typealias Address = EthereumAddress
33+
typealias UInt256 = BigUInt
34+
typealias UInt8 = Swift.UInt8
35+
typealias Bytes = Data
36+
}
37+
38+
extension EIP712.Address {
39+
static var zero: Self {
40+
EthereumAddress(Data(count: 20))!
41+
}
42+
}
43+
44+
extension EIP712Hashable {
45+
private var name: String {
46+
let fullName = "\(Self.self)"
47+
let name = fullName.components(separatedBy: ".").last ?? fullName
48+
return name
49+
}
50+
51+
private func dependencies() -> [EIP712Hashable] {
52+
let dependencies = Mirror(reflecting: self).children
53+
.compactMap { $0.value as? EIP712Hashable }
54+
.flatMap { [$0] + $0.dependencies() }
55+
return dependencies
56+
}
57+
58+
private func encodePrimaryType() -> String {
59+
let parametrs: [String] = Mirror(reflecting: self).children.compactMap { key, value in
60+
guard let key = key else { return nil }
61+
62+
func checkIfValueIsNil(value: Any) -> Bool {
63+
let mirror = Mirror(reflecting : value)
64+
if mirror.displayStyle == .optional {
65+
if mirror.children.count == 0 {
66+
return true
67+
}
68+
}
69+
70+
return false
71+
}
72+
73+
guard !checkIfValueIsNil(value: value) else { return nil }
74+
75+
let typeName: String
76+
switch value {
77+
case is EIP712.UInt8: typeName = "uint8"
78+
case is EIP712.UInt256: typeName = "uint256"
79+
case is EIP712.Address: typeName = "address"
80+
case is EIP712.Bytes: typeName = "bytes"
81+
case let hashable as EIP712Hashable: typeName = hashable.name
82+
default: typeName = "\(type(of: value))".lowercased()
83+
}
84+
return typeName + " " + key
85+
}
86+
return self.name + "(" + parametrs.joined(separator: ",") + ")"
87+
}
88+
89+
func encodeType() -> String {
90+
let dependencies = self.dependencies().map { $0.encodePrimaryType() }
91+
let selfPrimaryType = self.encodePrimaryType()
92+
93+
let result = Set(dependencies).filter { $0 != selfPrimaryType }
94+
return selfPrimaryType + result.sorted().joined()
95+
}
96+
97+
// MARK: - Default implementation
98+
99+
var typehash: Data {
100+
keccak256(encodeType())
101+
}
102+
103+
func hash() throws -> Data {
104+
typealias SolidityValue = (value: Any, type: ABI.Element.ParameterType)
105+
var parametrs: [Data] = [self.typehash]
106+
for case let (_, field) in Mirror(reflecting: self).children {
107+
let result: Data
108+
switch field {
109+
case let string as String:
110+
result = keccak256(string)
111+
case let data as EIP712.Bytes:
112+
result = keccak256(data)
113+
case is EIP712.UInt8:
114+
result = ABIEncoder.encodeSingleType(type: .uint(bits: 8), value: field as AnyObject)!
115+
case is EIP712.UInt256:
116+
result = ABIEncoder.encodeSingleType(type: .uint(bits: 256), value: field as AnyObject)!
117+
case is EIP712.Address:
118+
result = ABIEncoder.encodeSingleType(type: .address, value: field as AnyObject)!
119+
case let hashable as EIP712Hashable:
120+
result = try hashable.hash()
121+
default:
122+
if (field as AnyObject) is NSNull {
123+
continue
124+
} else {
125+
preconditionFailure("Not solidity type")
126+
}
127+
}
128+
guard result.count == 32 else { preconditionFailure("ABI encode error") }
129+
parametrs.append(result)
130+
}
131+
let encoded = parametrs.flatMap { $0.bytes }
132+
return keccak256(encoded)
133+
}
134+
}
135+
136+
// Encode functions
137+
func eip712encode(domainSeparator: EIP712Hashable, message: EIP712Hashable) throws -> Data {
138+
let data = try Data([UInt8(0x19), UInt8(0x01)]) + domainSeparator.hash() + message.hash()
139+
return keccak256(data)
140+
}
141+
142+
// MARK: - keccak256
143+
private func keccak256(_ data: [UInt8]) -> Data {
144+
Data(SHA3(variant: .keccak256).calculate(for: data))
145+
}
146+
147+
private func keccak256(_ string: String) -> Data {
148+
keccak256(Array(string.utf8))
149+
}
150+
151+
private func keccak256(_ data: Data) -> Data {
152+
keccak256(data.bytes)
153+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import XCTest
2+
@testable import web3swift
3+
4+
class EIP712Tests: XCTestCase {
5+
func testWithoutChainId() throws {
6+
7+
let to = EthereumAddress("0x3F06bAAdA68bB997daB03d91DBD0B73e196c5A4d")!
8+
9+
let value = EIP712.UInt256(0)
10+
11+
let amountLinen = EIP712.UInt256("0001000000000000000")//
12+
13+
let function = ABI.Element.Function(
14+
name: "approveAndMint",
15+
inputs: [
16+
.init(name: "cToken", type: .address),
17+
.init(name: "mintAmount", type: .uint(bits: 256))],
18+
outputs: [.init(name: "", type: .bool)],
19+
constant: false,
20+
payable: false)
21+
22+
let object = ABI.Element.function(function)
23+
24+
let safeTxData = object.encodeParameters([
25+
EthereumAddress("0x41B5844f4680a8C38fBb695b7F9CFd1F64474a72")! as AnyObject,
26+
amountLinen as AnyObject
27+
])!
28+
29+
let operation: EIP712.UInt8 = 1
30+
31+
let safeTxGas = EIP712.UInt256(250000)
32+
33+
let baseGas = EIP712.UInt256(60000)
34+
35+
let gasPrice = EIP712.UInt256("20000000000")
36+
37+
let gasToken = EthereumAddress("0x0000000000000000000000000000000000000000")!
38+
39+
let refundReceiver = EthereumAddress("0x7c07D32e18D6495eFDC487A32F8D20daFBa53A5e")!
40+
41+
let nonce: EIP712.UInt256 = .init(6)
42+
43+
let safeTX = SafeTx(
44+
to: to,
45+
value: value,
46+
data: safeTxData,
47+
operation: operation,
48+
safeTxGas: safeTxGas,
49+
baseGas: baseGas,
50+
gasPrice: gasPrice,
51+
gasToken: gasToken,
52+
refundReceiver: refundReceiver,
53+
nonce: nonce)
54+
55+
let password = ""
56+
let chainId: EIP712.UInt256? = nil
57+
let verifyingContract = EthereumAddress("0x40c21f00Faafcf10Cc671a75ea0de62305199DC1")!
58+
59+
let mnemonic = "normal dune pole key case cradle unfold require tornado mercy hospital buyer"
60+
let keystore = try! BIP32Keystore(mnemonics: mnemonic, password: "", mnemonicsPassword: "")!
61+
62+
let account = keystore.addresses?[0]
63+
64+
let signature = try Web3Signer.signEIP712(
65+
safeTx: safeTX,
66+
keystore: keystore,
67+
verifyingContract: verifyingContract,
68+
account: account!,
69+
password: password,
70+
chainId: chainId)
71+
72+
XCTAssertEqual(signature.toHexString(), "bf3182a3f52e65b416f86e76851c8e7d5602aef28af694f31359705b039d8d1931d53f3d5088ac7195944e8a9188d161ba3757877d08105885304f65282228c71c")
73+
}
74+
75+
func testWithChainId() throws {
76+
77+
let to = EthereumAddress("0x3F06bAAdA68bB997daB03d91DBD0B73e196c5A4d")!
78+
79+
let value = EIP712.UInt256(0)
80+
81+
let amount = EIP712.UInt256("0001000000000000000")
82+
83+
let function = ABI.Element.Function(
84+
name: "approveAndMint",
85+
inputs: [
86+
.init(name: "cToken", type: .address),
87+
.init(name: "mintAmount", type: .uint(bits: 256))],
88+
outputs: [.init(name: "", type: .bool)],
89+
constant: false,
90+
payable: false)
91+
92+
let object = ABI.Element.function(function)
93+
94+
let safeTxData = object.encodeParameters([
95+
EthereumAddress("0x41B5844f4680a8C38fBb695b7F9CFd1F64474a72")! as AnyObject,
96+
amount as AnyObject
97+
])!
98+
99+
let operation: EIP712.UInt8 = 1
100+
101+
let safeTxGas = EIP712.UInt256(250000)
102+
103+
let baseGas = EIP712.UInt256(60000)
104+
105+
let gasPrice = EIP712.UInt256("20000000000")
106+
107+
let gasToken = EthereumAddress("0x0000000000000000000000000000000000000000")!
108+
109+
let refundReceiver = EthereumAddress("0x7c07D32e18D6495eFDC487A32F8D20daFBa53A5e")!
110+
111+
let nonce: EIP712.UInt256 = .init(0)
112+
113+
let safeTX = SafeTx(
114+
to: to,
115+
value: value,
116+
data: safeTxData,
117+
operation: operation,
118+
safeTxGas: safeTxGas,
119+
baseGas: baseGas,
120+
gasPrice: gasPrice,
121+
gasToken: gasToken,
122+
refundReceiver: refundReceiver,
123+
nonce: nonce)
124+
125+
let mnemonic = "normal dune pole key case cradle unfold require tornado mercy hospital buyer"
126+
let keystore = try! BIP32Keystore(mnemonics: mnemonic, password: "", mnemonicsPassword: "")!
127+
128+
let verifyingContract = EthereumAddress("0x76106814dc6150b0fe510fbda4d2d877ac221270")!
129+
130+
let account = keystore.addresses?[0]
131+
let password = ""
132+
let chainId: EIP712.UInt256? = EIP712.UInt256(42)
133+
134+
let signature = try Web3Signer.signEIP712(
135+
safeTx: safeTX,
136+
keystore: keystore,
137+
verifyingContract: verifyingContract,
138+
account: account!,
139+
password: password,
140+
chainId: chainId)
141+
142+
XCTAssertEqual(signature.toHexString(), "f1f423cb23efad5035d4fb95c19cfcd46d4091f2bd924680b88c4f9edfa1fb3a4ce5fc5d169f354e3b464f45a425ed3f6203af06afbacdc5c8224a300ce9e6b21b")
143+
}
144+
}
145+

web3swift.xcodeproj/project.pbxproj

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,8 @@
188188
3AEF4ABF22C0B6BE00AC7929 /* Web3+Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AEF4ABE22C0B6BE00AC7929 /* Web3+Constants.swift */; };
189189
4E28AF5725258CE20065EE44 /* web3swift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1317BCE3218C50D100D6D095 /* web3swift.framework */; };
190190
4E2DFEF425485B53001AF561 /* KeystoreParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2DFEF325485B53001AF561 /* KeystoreParams.swift */; };
191+
CB50A52827060BD600D7E39B /* web3swift_EIP712_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB50A52727060BD600D7E39B /* web3swift_EIP712_Tests.swift */; };
192+
CB50A52A27060C5300D7E39B /* EIP712.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB50A52927060C5300D7E39B /* EIP712.swift */; };
191193
E22A911F241ED71A00EC1021 /* browser.min.js in Resources */ = {isa = PBXBuildFile; fileRef = E22A911E241ED71A00EC1021 /* browser.min.js */; };
192194
E252E67F26B403F500717C16 /* web3swift_helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = E252E67E26B403F500717C16 /* web3swift_helpers.swift */; };
193195
E2B76710241ED479007EBFE3 /* browser.js in Resources */ = {isa = PBXBuildFile; fileRef = E2B7670F241ED479007EBFE3 /* browser.js */; };
@@ -402,6 +404,8 @@
402404
3AA816412276E5A900F5DB52 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
403405
3AEF4ABE22C0B6BE00AC7929 /* Web3+Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Web3+Constants.swift"; sourceTree = "<group>"; };
404406
4E2DFEF325485B53001AF561 /* KeystoreParams.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeystoreParams.swift; sourceTree = "<group>"; };
407+
CB50A52727060BD600D7E39B /* web3swift_EIP712_Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = web3swift_EIP712_Tests.swift; sourceTree = "<group>"; };
408+
CB50A52927060C5300D7E39B /* EIP712.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EIP712.swift; sourceTree = "<group>"; };
405409
E22A911E241ED71A00EC1021 /* browser.min.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = browser.min.js; sourceTree = "<group>"; };
406410
E252E67E26B403F500717C16 /* web3swift_helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = web3swift_helpers.swift; sourceTree = "<group>"; };
407411
E2B7670F241ED479007EBFE3 /* browser.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = browser.js; sourceTree = "<group>"; };
@@ -456,6 +460,28 @@
456460
1317404621BE96D300208B8F /* web3swiftTests */ = {
457461
isa = PBXGroup;
458462
children = (
463+
3AA816122276E48300F5DB52 /* web3swift_AdvancedABIv2_Tests.swift */,
464+
3AA8160F2276E48300F5DB52 /* web3swift_EIP67_Tests.swift */,
465+
3AA8161A2276E48300F5DB52 /* web3swift_EIP681_Tests.swift */,
466+
CB50A52727060BD600D7E39B /* web3swift_EIP712_Tests.swift */,
467+
3AA8161E2276E48300F5DB52 /* web3swift_ENS_Tests.swift */,
468+
3AA816172276E48300F5DB52 /* web3swift_ERC20_Class_Tests.swift */,
469+
3AA816132276E48300F5DB52 /* web3swift_ERC20_Tests.swift */,
470+
3AA816222276E48400F5DB52 /* web3swift_Eventloop_Tests.swift */,
471+
3AA8161F2276E48400F5DB52 /* web3swift_infura_Tests.swift */,
472+
3AA8161B2276E48300F5DB52 /* web3swift_keystores_Tests.swift */,
473+
3AA816102276E48300F5DB52 /* web3swift_local_node_Tests.swift */,
474+
3AA816182276E48300F5DB52 /* web3swift_numberFormattingUtil_Tests.swift */,
475+
3AA816192276E48300F5DB52 /* web3swift_ObjC_Tests.swift */,
476+
3AA8161C2276E48300F5DB52 /* web3swift_promises_Tests.swift */,
477+
3AA8161D2276E48300F5DB52 /* web3swift_remoteParsing_Tests.swift */,
478+
3AA816212276E48400F5DB52 /* web3swift_rinkeby_personalSignature_Tests.swift */,
479+
3AA816112276E48300F5DB52 /* web3swift_RLP_Tests.swift */,
480+
3AA816142276E48300F5DB52 /* web3swift_ST20AndSecurityToken_Tests.swift */,
481+
3AA816152276E48300F5DB52 /* web3swift_Tests.swift */,
482+
3AA816202276E48400F5DB52 /* web3swift_transactions_Tests.swift */,
483+
3AA816162276E48300F5DB52 /* web3swift_User_cases.swift */,
484+
3AA816232276E48400F5DB52 /* web3swift_Websockets_Tests.swift */,
459485
E252E68126B542D000717C16 /* local_tests */,
460486
E252E68026B40C1600717C16 /* infura_tests */,
461487
);
@@ -650,6 +676,7 @@
650676
children = (
651677
3AA8152E2276E44100F5DB52 /* EIP67Code.swift */,
652678
3AA8152F2276E44100F5DB52 /* EIP681.swift */,
679+
CB50A52927060C5300D7E39B /* EIP712.swift */,
653680
);
654681
path = EIP;
655682
sourceTree = "<group>";
@@ -1364,6 +1391,8 @@
13641391
isa = PBXSourcesBuildPhase;
13651392
buildActionMask = 2147483647;
13661393
files = (
1394+
CB50A52A27060C5300D7E39B /* EIP712.swift in Sources */,
1395+
CB50A52827060BD600D7E39B /* web3swift_EIP712_Tests.swift in Sources */,
13671396
3AA8162E2276E48400F5DB52 /* web3swift_ObjC_Tests.swift in Sources */,
13681397
3AA816372276E48400F5DB52 /* web3swift_Eventloop_Tests.swift in Sources */,
13691398
3AA816252276E48400F5DB52 /* web3swift_basic_local_node_Tests.swift in Sources */,

0 commit comments

Comments
 (0)