Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Working with EIP-3009, transferWithAuthorization #146

Closed
danielnordh opened this issue Jun 15, 2021 · 12 comments
Closed

Working with EIP-3009, transferWithAuthorization #146

danielnordh opened this issue Jun 15, 2021 · 12 comments

Comments

@danielnordh
Copy link
Contributor

I'm trying to get our client to support EIP-3009, to let us submit transactions on our users' behalf.
Specifically USDC's 'transferWithAuthorization' function.

image

My assumptions for how the flow should go:

  1. Create a Data object that conforms to the function protocol described here. Not sure which existing web3.swift functions I can leverage here. Their javascript example below. (Do I include the type definitions in the data, or just the actual data - domain, primaryType, message?)

image

  1. Sign the Data object, hopefully using func sign(data: Data) throws -> Data (Will the result allow for extracting the v, r, s values?)
  2. Pass the necessary transaction parameters and signature to the server who will be submitting the transaction
  3. Extract the v, r, s values from the signature
  4. Call the USDC contract's transferWithAuthorization function with all the parameters

If you have any thoughts on errors in this flow, or ideas for how to best support EIP-3009 @DarthMike let me know.
I haven't been able to find much info on this, particularly in Swift. Not sure it has been done before.
Happy to submit any work I do here back to the repo.

@danielnordh
Copy link
Contributor Author

danielnordh commented Jun 16, 2021

Updated assumed flow:

  1. Create a TypedData object as defined in EIP-712 with a message that conforms to the transferWithAuthorization protocol
  2. Sign the TypedData with signMessage(message: TypedData)
  3. Pass the necessary transaction parameters and signature to the server who will be submitting the transaction
    Extract the v, r, s values from the signature
  4. Call the USDC contract's transferWithAuthorization function with all the parameters

Currently working on creating a correctly formatted TypedData object.

Wrestling with creating TypedVariable
let domainName = TypedVariable(name: "USDC", type: String)
gives the error 'Extra arguments at positions #1, #2 in call' (even after changing parameters from let to var)

@DarthMike
Copy link
Member

Hello @danielnordh,
Possibly that error is related to init not being public (did the change in #148, will merge shortly).

I think it's easier if you just use a JSON template and generate the message with a JSON string as we do in the tests. Constructing this JSON Is very verbose.

https://github.com/argentlabs/web3.swift/blob/master/web3sTests/Account/EthereumAccount%2BSignTypedTests.swift

@danielnordh
Copy link
Contributor Author

Thanks @DarthMike, the changes in #148 should probably take care of that issue 👍
I was trying to avoid JSON as there are so many dynamic properties, but I guess I would have to take that route if I can't make this work.

@danielnordh
Copy link
Contributor Author

OK I think I solved my problem for now, mostly using your suggested JSON solution but with some added convenience functions and structs. Sharing rough concept here for anyone else trying to achieve the same thing.

Flow

let domain = EIP712Domain(name: ..., version: ..., chainId: ..., verifyingContract: ...)
let parameters = TransferWithAuthorizationParameters(from: ..., to: ..., value: ..., validAfter: ..., validBefore: ..., nonce: ...)
let transferData = transferWithAuthorizationData(domain: domain, parameters: parameters)
let decoder = JSONDecoder()
let typedData = try! decoder.decode(TypedData.self, from: transferData)
let signature = try? BlockchainClient.shared.account.signMessage(message: typedData)

Structs

public struct EIP712Domain: Codable {
   var name: String
   var version: String
   var chainId: BigUInt
   var verifyingContract: EthereumAddress
}

public struct TransferWithAuthorizationParameters: Codable {
   var from: EthereumAddress
    var to: EthereumAddress
    var value: BigUInt
    var validAfter: BigUInt
    var validBefore: BigUInt
    var nonce: String
}

Helper method for jsonData

public func transferWithAuthorizationData(domain: EIP712Domain, parameters: TransferWithAuthorizationParameters) -> Data {
    let transferData = """
       {
         "types": {
           "EIP712Domain": [
             {"name": "name", "type": "string"},
             {"name": "version", "type": "string"},
             {"name": "chainId", "type": "uint256"},
             {"name": "verifyingContract", "type": "address"}
           ],
          "TransferWithAuthorization": [
            {"name": "from", "type": "address"},
            {"name": "to", "type": "address"},
            {"name": "value", "type": "uint256"},
            {"name": "validAfter", "type": "uint256"},
            {"name": "validBefore", "type": "uint256"},
            {"name": "nonce", "type": "bytes32"},
           ]
         },
         "primaryType": "TransferWithAuthorization",
         "domain": {
           "name": "\(domain.name)",
           "version": "\(domain.version)",
           "chainId": \(domain.chainId.description),
           "verifyingContract": "\(domain.verifyingContract.value)"
         },
         "message": {
           "from": "\(parameters.from.value)",
           "to": "\(parameters.to.value)",
           "value": "\(parameters.value.description)",
           "validAfter": "\(parameters.validAfter.description)",
           "validBefore": "\(parameters.validBefore.description)",
           "nonce": "\(parameters.nonce)"
         }
       }
   """.data(using: .utf8)!
   return transferData
}

For generating random nonce

func randomNonce() -> String? {
    var bytes = [UInt8](repeating: 0, count: 32)
    let result = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)

    guard result == errSecSuccess else {
        print("Problem generating random bytes")
        return nil
    }

    return "0x" + Data(bytes).hexEncodedString()
}

extension Data {
    struct HexEncodingOptions: OptionSet {
        let rawValue: Int
        static let upperCase = HexEncodingOptions(rawValue: 1 << 0)
}

func hexEncodedString(options: HexEncodingOptions = []) -> String {
        let format = options.contains(.upperCase) ? "%02hhX" : "%02hhx"
        return self.map { String(format: format, $0) }.joined()
    }
}

@omidziaee
Copy link

Hi,
Quick question, after you get the v, r, s how do you submit the transaction? Do you use the contract method "transferWithAuthorization"? Could you please elaborate more on that with an example?

@danielnordh
Copy link
Contributor Author

Here's an overview of how I used this in our code to make a USDC transfer:

// TransferWithAuthorization, create signed transaction
// 1. Create the data that needs to be signed
        
let domain = EIP712Domain(name: "USDC", version: "2", chainId: BigUInt(CHAIN_ID), verifyingContract: EthereumAddress(USDC_CONTRACT))
let parameters = TransferWithAuthorizationParameters(from: signing_address, to: receiving_address, value: amount, validAfter: BigUInt.zero, validBefore: BigUInt(9223372036854775807), nonce: randomNonce()?.description ?? "")
let transferData = transferWithAuthorizationData(domain: domain, parameters: parameters)

let decoder = JSONDecoder()
let typedData = try? decoder.decode(TypedData.self, from: transferData)
        
// 2. Sign with 712

do {
  let signature = try BlockchainClient.shared.account.signMessage(message: typedData!)
     
  // 3. Submit to server for broadcasting
  // send the signature to where it will be broadcast from, in our case it was a backend server
    
  } catch {
      // Handle error
}

@magus777
Copy link

What does JSONDecoder() do ?
What would be the equivalent in Javascript?

@danielnordh
Copy link
Contributor Author

What does JSONDecoder() do ?
What would be the equivalent in Javascript?

Check out this overview in JS:
https://gist.github.com/markodayan/e05f524b915f129c4f8500df816a369b

You need to turn your JSON into data, example from the link above:

const data = JSON.stringify({
    types: {
        EIP712Domain: domain,
        Bid: bid,
        Identity: identity,
    },
    domain: domainData,
    primaryType: "Bid",
    message: message
});

@magus777
Copy link

I installed the EIP-712 node module:
https://github.com/Mrtenz/eip-712

And now I'm getting this error:

node_modules/eip-712/lib/cjs/eip-712.js:90
const [types, values] = typedData.types[type].reduce(([types, values], field) => {
^

TypeError: Cannot read properties of undefined (reading 'EIP712Domain')

I noticed in one of your examples, you cast the data to UTF8. Is this the problem?

@danielnordh
Copy link
Contributor Author

I noticed in one of your examples, you cast the data to UTF8. Is this the problem?

It could be.
It was quite a while I worked on this and don't have fresh knowledge and won't be able to figure it out for you.
You just have to keep testing different changes, starting with a working example.

@magus777
Copy link

magus777 commented Sep 26, 2023

For anyone else struggling with this. I finally found some useful information:

How to make EIP712 typed data object:

https://medium.com/@ashwin.yar/eip-712-structured-data-hashing-and-signing-explained-c8ad00874486

Problems & Solutions..

web3/web3.js#5927

https://ethereum.stackexchange.com/questions/74027/eip712-implementation-for-web3-1-0

https://ethereum.stackexchange.com/questions/129707/get-signature-for-tuple-input-for-eip712

eth_signTypedData and signTypedData_v4 function only exists in the canary (dev version) of web3:
https://www.npmjs.com/package/web3/v/4.0.4-dev.933ef51.0

You need to use this metamask module: eth-sig-util and the function:

https://metamask.github.io/eth-sig-util/latest/functions/signTypedData.html

I see the light at the end of the tunnel...

@magus777
Copy link

To help anyone else who stumbles here:
(I finally got transferWithAuthorization to work. :-) )

This will probably save you about 2 nights of sleep...

const buffer1 = Buffer.from(user_privateKey.substring(2,66), "hex");
const cust_signature = ethSigUtil.signTypedData({data: data, privateKey: buffer1, version: 'V4'});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants