From 419c6ba4f6d64a02c2d0ade684bc69c3022dc73a Mon Sep 17 00:00:00 2001 From: Andrey Maksimkin <> Date: Thu, 30 Sep 2021 18:18:09 +0300 Subject: [PATCH] add support of EIP-712 signature --- .../Transaction/TransactionSigner.swift | 19 +++ Sources/web3swift/Utils/EIP/EIP712.swift | 152 ++++++++++++++++++ .../web3swift_EIP712_Tests.swift | 145 +++++++++++++++++ web3swift.xcodeproj/project.pbxproj | 8 + 4 files changed, 324 insertions(+) create mode 100644 Sources/web3swift/Utils/EIP/EIP712.swift create mode 100644 Tests/web3swiftTests/web3swift_EIP712_Tests.swift diff --git a/Sources/web3swift/Transaction/TransactionSigner.swift b/Sources/web3swift/Transaction/TransactionSigner.swift index b5349c7a8..6b2a8b28a 100755 --- a/Sources/web3swift/Transaction/TransactionSigner.swift +++ b/Sources/web3swift/Transaction/TransactionSigner.swift @@ -111,6 +111,25 @@ public struct Web3Signer { } } + public static func signEIP712(safeTx: SafeTx, + keystore: BIP32Keystore, + verifyingContract: EthereumAddress, + account: EthereumAddress, + password: String? = nil, + chainId: BigUInt? = nil) throws -> Data { + + let domainSeparator: EIP712DomainHashable = EIP712Domain(chainId: chainId, verifyingContract: verifyingContract) + + let password = password ?? "" + let hash = try eip712encode(domainSeparator: domainSeparator, message: safeTx) + + guard let signature = try Web3Signer.signPersonalMessage(hash, keystore: keystore, account: account, password: password) else { + throw Web3Error.dataError + } + + return signature; + } + } diff --git a/Sources/web3swift/Utils/EIP/EIP712.swift b/Sources/web3swift/Utils/EIP/EIP712.swift new file mode 100644 index 000000000..24fcc4672 --- /dev/null +++ b/Sources/web3swift/Utils/EIP/EIP712.swift @@ -0,0 +1,152 @@ +import BigInt +import CryptoSwift + +struct EIP712Domain: EIP712DomainHashable { + let chainId: EIP712.UInt256? + let verifyingContract: EIP712.Address +} + +protocol EIP712DomainHashable: EIP712Hashable {} + +public struct SafeTx: EIP712Hashable { + let to: EIP712.Address + let value: EIP712.UInt256 + let data: EIP712.Bytes + let operation: EIP712.UInt8 + let safeTxGas: EIP712.UInt256 + let baseGas: EIP712.UInt256 + let gasPrice: EIP712.UInt256 + let gasToken: EIP712.Address + let refundReceiver: EIP712.Address + let nonce: EIP712.UInt256 +} + +/// Protocol defines EIP712 struct encoding +protocol EIP712Hashable { + var typehash: Data { get } + func hash() throws -> Data +} + +class EIP712 { + typealias Address = EthereumAddress + typealias UInt256 = BigUInt + typealias UInt8 = Swift.UInt8 + typealias Bytes = Data +} + +extension EIP712.Address { + static var zero: Self { + EthereumAddress(Data(count: 20))! + } +} + +extension EIP712Hashable { + private var name: String { + let fullName = "\(Self.self)" + let name = fullName.components(separatedBy: ".").last ?? fullName + return name + } + + private func dependencies() -> [EIP712Hashable] { + let dependencies = Mirror(reflecting: self).children + .compactMap { $0.value as? EIP712Hashable } + .flatMap { [$0] + $0.dependencies() } + return dependencies + } + + private func encodePrimaryType() -> String { + let parametrs: [String] = Mirror(reflecting: self).children.compactMap { key, value in + guard let key = key else { return nil } + + func checkIfValueIsNil(value: Any) -> Bool { + let mirror = Mirror(reflecting : value) + if mirror.displayStyle == .optional { + if mirror.children.count == 0 { + return true + } + } + + return false + } + + guard !checkIfValueIsNil(value: value) else { return nil } + + let typeName: String + switch value { + case is EIP712.UInt8: typeName = "uint8" + case is EIP712.UInt256: typeName = "uint256" + case is EIP712.Address: typeName = "address" + case is EIP712.Bytes: typeName = "bytes" + case let hashable as EIP712Hashable: typeName = hashable.name + default: typeName = "\(type(of: value))".lowercased() + } + return typeName + " " + key + } + return self.name + "(" + parametrs.joined(separator: ",") + ")" + } + + func encodeType() -> String { + let dependencies = self.dependencies().map { $0.encodePrimaryType() } + let selfPrimaryType = self.encodePrimaryType() + + let result = Set(dependencies).filter { $0 != selfPrimaryType } + return selfPrimaryType + result.sorted().joined() + } + + // MARK: - Default implementation + + var typehash: Data { + keccak256(encodeType()) + } + + func hash() throws -> Data { + typealias SolidityValue = (value: Any, type: ABI.Element.ParameterType) + var parametrs: [Data] = [self.typehash] + for case let (_, field) in Mirror(reflecting: self).children { + let result: Data + switch field { + case let string as String: + result = keccak256(string) + case let data as EIP712.Bytes: + result = keccak256(data) + case is EIP712.UInt8: + result = ABIEncoder.encodeSingleType(type: .uint(bits: 8), value: field as AnyObject)! + case is EIP712.UInt256: + result = ABIEncoder.encodeSingleType(type: .uint(bits: 256), value: field as AnyObject)! + case is EIP712.Address: + result = ABIEncoder.encodeSingleType(type: .address, value: field as AnyObject)! + case let hashable as EIP712Hashable: + result = try hashable.hash() + default: + if (field as AnyObject) is NSNull { + continue + } else { + preconditionFailure("Not solidity type") + } + } + guard result.count == 32 else { preconditionFailure("ABI encode error") } + parametrs.append(result) + } + let encoded = parametrs.flatMap { $0.bytes } + return keccak256(encoded) + } +} + +// Encode functions +func eip712encode(domainSeparator: EIP712Hashable, message: EIP712Hashable) throws -> Data { + let data = try Data([UInt8(0x19), UInt8(0x01)]) + domainSeparator.hash() + message.hash() + return keccak256(data) +} + +// MARK: - keccak256 +private func keccak256(_ data: [UInt8]) -> Data { + Data(SHA3(variant: .keccak256).calculate(for: data)) +} + +private func keccak256(_ string: String) -> Data { + keccak256(Array(string.utf8)) +} + +private func keccak256(_ data: Data) -> Data { + keccak256(data.bytes) +} diff --git a/Tests/web3swiftTests/web3swift_EIP712_Tests.swift b/Tests/web3swiftTests/web3swift_EIP712_Tests.swift new file mode 100644 index 000000000..acc1f9d0f --- /dev/null +++ b/Tests/web3swiftTests/web3swift_EIP712_Tests.swift @@ -0,0 +1,145 @@ +import XCTest +@testable import web3swift + +class EIP712Tests: XCTestCase { + func testWithoutChainId() throws { + + let to = EthereumAddress("0x3F06bAAdA68bB997daB03d91DBD0B73e196c5A4d")! + + let value = EIP712.UInt256(0) + + let amountLinen = EIP712.UInt256("0001000000000000000")// + + let function = ABI.Element.Function( + name: "approveAndMint", + inputs: [ + .init(name: "cToken", type: .address), + .init(name: "mintAmount", type: .uint(bits: 256))], + outputs: [.init(name: "", type: .bool)], + constant: false, + payable: false) + + let object = ABI.Element.function(function) + + let safeTxData = object.encodeParameters([ + EthereumAddress("0x41B5844f4680a8C38fBb695b7F9CFd1F64474a72")! as AnyObject, + amountLinen as AnyObject + ])! + + let operation: EIP712.UInt8 = 1 + + let safeTxGas = EIP712.UInt256(250000) + + let baseGas = EIP712.UInt256(60000) + + let gasPrice = EIP712.UInt256("20000000000") + + let gasToken = EthereumAddress("0x0000000000000000000000000000000000000000")! + + let refundReceiver = EthereumAddress("0x7c07D32e18D6495eFDC487A32F8D20daFBa53A5e")! + + let nonce: EIP712.UInt256 = .init(6) + + let safeTX = SafeTx( + to: to, + value: value, + data: safeTxData, + operation: operation, + safeTxGas: safeTxGas, + baseGas: baseGas, + gasPrice: gasPrice, + gasToken: gasToken, + refundReceiver: refundReceiver, + nonce: nonce) + + let password = "" + let chainId: EIP712.UInt256? = nil + let verifyingContract = EthereumAddress("0x40c21f00Faafcf10Cc671a75ea0de62305199DC1")! + + let mnemonic = "normal dune pole key case cradle unfold require tornado mercy hospital buyer" + let keystore = try! BIP32Keystore(mnemonics: mnemonic, password: "", mnemonicsPassword: "")! + + let account = keystore.addresses?[0] + + let signature = try Web3Signer.signEIP712( + safeTx: safeTX, + keystore: keystore, + verifyingContract: verifyingContract, + account: account!, + password: password, + chainId: chainId) + + XCTAssertEqual(signature.toHexString(), "bf3182a3f52e65b416f86e76851c8e7d5602aef28af694f31359705b039d8d1931d53f3d5088ac7195944e8a9188d161ba3757877d08105885304f65282228c71c") + } + + func testWithChainId() throws { + + let to = EthereumAddress("0x3F06bAAdA68bB997daB03d91DBD0B73e196c5A4d")! + + let value = EIP712.UInt256(0) + + let amount = EIP712.UInt256("0001000000000000000") + + let function = ABI.Element.Function( + name: "approveAndMint", + inputs: [ + .init(name: "cToken", type: .address), + .init(name: "mintAmount", type: .uint(bits: 256))], + outputs: [.init(name: "", type: .bool)], + constant: false, + payable: false) + + let object = ABI.Element.function(function) + + let safeTxData = object.encodeParameters([ + EthereumAddress("0x41B5844f4680a8C38fBb695b7F9CFd1F64474a72")! as AnyObject, + amount as AnyObject + ])! + + let operation: EIP712.UInt8 = 1 + + let safeTxGas = EIP712.UInt256(250000) + + let baseGas = EIP712.UInt256(60000) + + let gasPrice = EIP712.UInt256("20000000000") + + let gasToken = EthereumAddress("0x0000000000000000000000000000000000000000")! + + let refundReceiver = EthereumAddress("0x7c07D32e18D6495eFDC487A32F8D20daFBa53A5e")! + + let nonce: EIP712.UInt256 = .init(0) + + let safeTX = SafeTx( + to: to, + value: value, + data: safeTxData, + operation: operation, + safeTxGas: safeTxGas, + baseGas: baseGas, + gasPrice: gasPrice, + gasToken: gasToken, + refundReceiver: refundReceiver, + nonce: nonce) + + let mnemonic = "normal dune pole key case cradle unfold require tornado mercy hospital buyer" + let keystore = try! BIP32Keystore(mnemonics: mnemonic, password: "", mnemonicsPassword: "")! + + let verifyingContract = EthereumAddress("0x76106814dc6150b0fe510fbda4d2d877ac221270")! + + let account = keystore.addresses?[0] + let password = "" + let chainId: EIP712.UInt256? = EIP712.UInt256(42) + + let signature = try Web3Signer.signEIP712( + safeTx: safeTX, + keystore: keystore, + verifyingContract: verifyingContract, + account: account!, + password: password, + chainId: chainId) + + XCTAssertEqual(signature.toHexString(), "f1f423cb23efad5035d4fb95c19cfcd46d4091f2bd924680b88c4f9edfa1fb3a4ce5fc5d169f354e3b464f45a425ed3f6203af06afbacdc5c8224a300ce9e6b21b") + } +} + diff --git a/web3swift.xcodeproj/project.pbxproj b/web3swift.xcodeproj/project.pbxproj index d18df8ac1..6fec95f2a 100755 --- a/web3swift.xcodeproj/project.pbxproj +++ b/web3swift.xcodeproj/project.pbxproj @@ -187,6 +187,8 @@ 3AEF4ABF22C0B6BE00AC7929 /* Web3+Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AEF4ABE22C0B6BE00AC7929 /* Web3+Constants.swift */; }; 4E28AF5725258CE20065EE44 /* web3swift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1317BCE3218C50D100D6D095 /* web3swift.framework */; }; 4E2DFEF425485B53001AF561 /* KeystoreParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2DFEF325485B53001AF561 /* KeystoreParams.swift */; }; + CB50A52827060BD600D7E39B /* web3swift_EIP712_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB50A52727060BD600D7E39B /* web3swift_EIP712_Tests.swift */; }; + CB50A52A27060C5300D7E39B /* EIP712.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB50A52927060C5300D7E39B /* EIP712.swift */; }; E22A911F241ED71A00EC1021 /* browser.min.js in Resources */ = {isa = PBXBuildFile; fileRef = E22A911E241ED71A00EC1021 /* browser.min.js */; }; E2B76710241ED479007EBFE3 /* browser.js in Resources */ = {isa = PBXBuildFile; fileRef = E2B7670F241ED479007EBFE3 /* browser.js */; }; E2EDC5EA241EDE3600410EA6 /* BrowserViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EDC5E9241EDE3600410EA6 /* BrowserViewController.swift */; }; @@ -390,6 +392,8 @@ 3AA816412276E5A900F5DB52 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 3AEF4ABE22C0B6BE00AC7929 /* Web3+Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Web3+Constants.swift"; sourceTree = ""; }; 4E2DFEF325485B53001AF561 /* KeystoreParams.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeystoreParams.swift; sourceTree = ""; }; + CB50A52727060BD600D7E39B /* web3swift_EIP712_Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = web3swift_EIP712_Tests.swift; sourceTree = ""; }; + CB50A52927060C5300D7E39B /* EIP712.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EIP712.swift; sourceTree = ""; }; E22A911E241ED71A00EC1021 /* browser.min.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = browser.min.js; sourceTree = ""; }; E2B7670F241ED479007EBFE3 /* browser.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = browser.js; sourceTree = ""; }; E2D081C72402F6900082EA93 /* CONTRIBUTION_POLICY.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CONTRIBUTION_POLICY.md; sourceTree = ""; }; @@ -431,6 +435,7 @@ 3AA816122276E48300F5DB52 /* web3swift_AdvancedABIv2_Tests.swift */, 3AA8160F2276E48300F5DB52 /* web3swift_EIP67_Tests.swift */, 3AA8161A2276E48300F5DB52 /* web3swift_EIP681_Tests.swift */, + CB50A52727060BD600D7E39B /* web3swift_EIP712_Tests.swift */, 3AA8161E2276E48300F5DB52 /* web3swift_ENS_Tests.swift */, 3AA816172276E48300F5DB52 /* web3swift_ERC20_Class_Tests.swift */, 3AA816132276E48300F5DB52 /* web3swift_ERC20_Tests.swift */, @@ -636,6 +641,7 @@ children = ( 3AA8152E2276E44100F5DB52 /* EIP67Code.swift */, 3AA8152F2276E44100F5DB52 /* EIP681.swift */, + CB50A52927060C5300D7E39B /* EIP712.swift */, ); path = EIP; sourceTree = ""; @@ -1313,6 +1319,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + CB50A52A27060C5300D7E39B /* EIP712.swift in Sources */, + CB50A52827060BD600D7E39B /* web3swift_EIP712_Tests.swift in Sources */, 3AA8162E2276E48400F5DB52 /* web3swift_ObjC_Tests.swift in Sources */, 3AA816372276E48400F5DB52 /* web3swift_Eventloop_Tests.swift in Sources */, 3AA816252276E48400F5DB52 /* web3swift_local_node_Tests.swift in Sources */,