Skip to content

Commit

Permalink
Merge pull request #1 from AndreyMaksimkin/EIP712_signature
Browse files Browse the repository at this point in the history
add support of EIP-712 signature
  • Loading branch information
frostiq authored Sep 30, 2021
2 parents 5484e81 + 419c6ba commit d0520ca
Show file tree
Hide file tree
Showing 4 changed files with 324 additions and 0 deletions.
19 changes: 19 additions & 0 deletions Sources/web3swift/Transaction/TransactionSigner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

}


Expand Down
152 changes: 152 additions & 0 deletions Sources/web3swift/Utils/EIP/EIP712.swift
Original file line number Diff line number Diff line change
@@ -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)
}
145 changes: 145 additions & 0 deletions Tests/web3swiftTests/web3swift_EIP712_Tests.swift
Original file line number Diff line number Diff line change
@@ -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")
}
}

8 changes: 8 additions & 0 deletions web3swift.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -390,6 +392,8 @@
3AA816412276E5A900F5DB52 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
3AEF4ABE22C0B6BE00AC7929 /* Web3+Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Web3+Constants.swift"; sourceTree = "<group>"; };
4E2DFEF325485B53001AF561 /* KeystoreParams.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeystoreParams.swift; sourceTree = "<group>"; };
CB50A52727060BD600D7E39B /* web3swift_EIP712_Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = web3swift_EIP712_Tests.swift; sourceTree = "<group>"; };
CB50A52927060C5300D7E39B /* EIP712.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EIP712.swift; sourceTree = "<group>"; };
E22A911E241ED71A00EC1021 /* browser.min.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = browser.min.js; sourceTree = "<group>"; };
E2B7670F241ED479007EBFE3 /* browser.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = browser.js; sourceTree = "<group>"; };
E2D081C72402F6900082EA93 /* CONTRIBUTION_POLICY.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CONTRIBUTION_POLICY.md; sourceTree = "<group>"; };
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -636,6 +641,7 @@
children = (
3AA8152E2276E44100F5DB52 /* EIP67Code.swift */,
3AA8152F2276E44100F5DB52 /* EIP681.swift */,
CB50A52927060C5300D7E39B /* EIP712.swift */,
);
path = EIP;
sourceTree = "<group>";
Expand Down Expand Up @@ -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 */,
Expand Down

0 comments on commit d0520ca

Please sign in to comment.