-
Notifications
You must be signed in to change notification settings - Fork 445
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from AndreyMaksimkin/EIP712_signature
add support of EIP-712 signature
- Loading branch information
Showing
4 changed files
with
324 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters