Skip to content

Commit ce459b6

Browse files
feat(EIP712): type encoding, type hashing, circular dependency checks, tests;
1 parent a9b23fb commit ce459b6

File tree

2 files changed

+197
-2
lines changed

2 files changed

+197
-2
lines changed

Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ public class EIP712Parser {
9898
throw Web3Error.inputError(desc: "EIP712Parser: cannot decode EIP712TypedData object. Failed to parse one of primaryType, domain or message fields. Is any field missing?")
9999
}
100100

101-
return EIP712TypedData(types: types, primaryType: primaryType, domain: domain, message: message)
101+
return try EIP712TypedData(types: types, primaryType: primaryType, domain: domain, message: message)
102102
}
103103
}
104104

@@ -130,11 +130,107 @@ public struct EIP712TypedData {
130130
public init(types: [String : [EIP712TypeProperty]],
131131
primaryType: String,
132132
domain: [String : AnyObject],
133-
message: [String : AnyObject]) {
133+
message: [String : AnyObject]) throws {
134134
self.types = types
135135
self.primaryType = primaryType
136136
self.domain = domain
137137
self.message = message
138+
if let problematicType = hasCircularDependency() {
139+
throw Web3Error.inputError(desc: "Created EIP712TypedData has a circular dependency amongst it's types. Cycle was first identified in '\(problematicType)'. Review it's uses in 'types'.")
140+
}
138141
}
139142

143+
/// Checks for a circular dependency among the given types.
144+
///
145+
/// If a circular dependency is detected, it returns the name of the type where the cycle was first identified.
146+
/// Otherwise, it returns `nil`.
147+
///
148+
/// - Returns: The type name where a circular dependency is detected, or `nil` if no circular dependency exists.
149+
/// - Note: The function utilizes depth-first search to identify the circular dependencies.
150+
func hasCircularDependency() -> String? {
151+
152+
/// Generates an adjacency list for the given types, representing their dependencies.
153+
///
154+
/// - Parameter types: A dictionary mapping type names to their property definitions.
155+
/// - Returns: An adjacency list representing type dependencies.
156+
func createAdjacencyList(types: [String: [EIP712TypeProperty]]) -> [String: [String]] {
157+
var adjList: [String: [String]] = [:]
158+
159+
for (typeName, fields) in types {
160+
adjList[typeName] = []
161+
for field in fields {
162+
if types.keys.contains(field.type) {
163+
adjList[typeName]?.append(field.type)
164+
}
165+
}
166+
}
167+
168+
return adjList
169+
}
170+
171+
let adjList = createAdjacencyList(types: types)
172+
173+
/// Depth-first search to check for circular dependencies.
174+
///
175+
/// - Parameters:
176+
/// - node: The current type being checked.
177+
/// - visited: A dictionary keeping track of the visited types.
178+
/// - stack: A dictionary used for checking the current path for cycles.
179+
///
180+
/// - Returns: `true` if a cycle is detected from the current node, `false` otherwise.
181+
func depthFirstSearch(node: String, visited: inout [String: Bool], stack: inout [String: Bool]) -> Bool {
182+
visited[node] = true
183+
stack[node] = true
184+
185+
for neighbor in adjList[node] ?? [] {
186+
if visited[neighbor] == nil {
187+
if depthFirstSearch(node: neighbor, visited: &visited, stack: &stack) {
188+
return true
189+
}
190+
} else if stack[neighbor] == true {
191+
return true
192+
}
193+
}
194+
195+
stack[node] = false
196+
return false
197+
}
198+
199+
var visited: [String: Bool] = [:]
200+
var stack: [String: Bool] = [:]
201+
202+
for typeName in adjList.keys {
203+
if visited[typeName] == nil {
204+
if depthFirstSearch(node: typeName, visited: &visited, stack: &stack) {
205+
return typeName
206+
}
207+
}
208+
}
209+
210+
return nil
211+
}
212+
213+
public func encodeType(_ type: String) throws -> String {
214+
guard let typeData = types[type] else {
215+
throw Web3Error.processingError(desc: "EIP712. Attempting to encode type that doesn't exist in this payload. Given type: \(type). Available types: \(types.values).")
216+
}
217+
return try encodeType(type, typeData)
218+
}
219+
220+
public func typeHash(_ type: String) throws -> String {
221+
try encodeType(type).sha3(.keccak256).addHexPrefix()
222+
}
223+
224+
internal func encodeType(_ type: String, _ typeData: [EIP712TypeProperty], typesCovered: [String] = []) throws -> String {
225+
var typesCovered = typesCovered
226+
var encodedSubtypes: [String] = []
227+
let parameters = try typeData.map { attributeType in
228+
if let innerTypes = types[attributeType.type], !typesCovered.contains(attributeType.type) {
229+
encodedSubtypes.append(try encodeType(attributeType.type, innerTypes))
230+
typesCovered.append(attributeType.type)
231+
}
232+
return "\(attributeType.type) \(attributeType.name)"
233+
}
234+
return type + "(" + parameters.joined(separator: ",") + ")" + encodedSubtypes.joined(separator: "")
235+
}
140236
}

Tests/web3swiftTests/localTests/EIP712TypedDataPayloadTests.swift

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,4 +120,103 @@ class EIP712TypedDataPayloadTests: XCTestCase {
120120
"wallet" : "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"])
121121
XCTAssertEqual(parsedEip712TypedData.message["contents"] as? String, "Hello, Bob!")
122122
}
123+
124+
func testEIP712CircularDependency() throws {
125+
let problematicTypeExample = """
126+
{
127+
"types":{
128+
"EIP712Domain":[
129+
{
130+
"name":"name",
131+
"type":"string"
132+
},
133+
{
134+
"name":"version",
135+
"type":"string"
136+
},
137+
{
138+
"name":"chainId",
139+
"type":"uint256"
140+
},
141+
{
142+
"name":"verifyingContract",
143+
"type":"address"
144+
}
145+
],
146+
"Person":[
147+
{
148+
"name":"name",
149+
"type":"string"
150+
},
151+
{
152+
"name":"wallet",
153+
"type":"address"
154+
},
155+
{
156+
"name":"mail",
157+
"type":"Mail"
158+
}
159+
],
160+
"Mail":[
161+
{
162+
"name":"from",
163+
"type":"Person"
164+
},
165+
{
166+
"name":"to",
167+
"type":"Person"
168+
},
169+
{
170+
"name":"contents",
171+
"type":"string"
172+
}
173+
]
174+
},
175+
"primaryType":"Mail",
176+
"domain":{
177+
"name":"Ether Mail",
178+
"version":"1",
179+
"chainId":1,
180+
"verifyingContract":"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
181+
},
182+
"message":{
183+
"from":{
184+
"name":"Cow",
185+
"wallet":"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
186+
},
187+
"to":{
188+
"name":"Bob",
189+
"wallet":"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
190+
},
191+
"contents":"Hello, Bob!"
192+
}
193+
}
194+
"""
195+
XCTAssertThrowsError(try EIP712Parser.parse(problematicTypeExample)) { error in
196+
guard let error = error as? Web3Error else {
197+
XCTFail("Thrown error is not Web3Error.")
198+
return
199+
}
200+
201+
if case let .inputError(desc) = error {
202+
XCTAssertTrue(desc.hasPrefix("Created EIP712TypedData has a circular dependency amongst it's types."))
203+
} else {
204+
XCTFail("A different Web3Error is thrown. Something changed?")
205+
}
206+
}
207+
}
208+
209+
func testEIP712EncodeType() throws {
210+
let parsedEip712TypedData = try EIP712Parser.parse(testTypedDataPayload)
211+
try XCTAssertEqual(parsedEip712TypedData.encodeType("EIP712Domain"), "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")
212+
try XCTAssertEqual(parsedEip712TypedData.encodeType("Person"), "Person(string name,address wallet)")
213+
try XCTAssertEqual(parsedEip712TypedData.encodeType("Mail"), "Mail(Person from,Person to,string contents)Person(string name,address wallet)")
214+
}
215+
216+
func testEIP712TypeHash() throws {
217+
let parsedEip712TypedData = try EIP712Parser.parse(testTypedDataPayload)
218+
try XCTAssertEqual(parsedEip712TypedData.typeHash("EIP712Domain"), "0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f")
219+
try XCTAssertEqual(parsedEip712TypedData.typeHash("Person"), "0xb9d8c78acf9b987311de6c7b45bb6a9c8e1bf361fa7fd3467a2163f994c79500")
220+
try XCTAssertEqual(parsedEip712TypedData.typeHash("Mail"), "0xa0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2")
221+
}
123222
}

0 commit comments

Comments
 (0)