Module: Mpp::Methods::Evm::Authorization

Defined in:
lib/mpp/methods/evm/authorization.rb

Overview

EIP-712 TransferWithAuthorization hashing and recovery.

Constant Summary collapse

DOMAIN_TYPE_HASH =
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
TRANSFER_TYPE_HASH =
"TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)"

Class Method Summary collapse

Class Method Details

.abi_encode(*values) ⇒ Object



102
103
104
# File 'lib/mpp/methods/evm/authorization.rb', line 102

def abi_encode(*values)
  values.join
end

.address_word(value) ⇒ Object

Raises:

  • (ArgumentError)


113
114
115
116
117
118
# File 'lib/mpp/methods/evm/authorization.rb', line 113

def address_word(value)
  hex = value.to_s.delete_prefix("0x")
  raise ArgumentError, "invalid address: #{value}" unless hex.match?(/\A[a-fA-F0-9]{40}\z/)

  [hex].pack("H*").rjust(32, "\x00".b)
end

.bytes32_word(value) ⇒ Object

Raises:

  • (ArgumentError)


120
121
122
123
124
125
# File 'lib/mpp/methods/evm/authorization.rb', line 120

def bytes32_word(value)
  hex = value.to_s.delete_prefix("0x")
  raise ArgumentError, "invalid bytes32: #{value}" unless hex.match?(/\A[a-fA-F0-9]{64}\z/)

  [hex].pack("H*")
end

.challenge_hash(challenge) ⇒ Object



21
22
23
24
# File 'lib/mpp/methods/evm/authorization.rb', line 21

def challenge_hash(challenge)
  digest = keccak256("#{challenge.id}#{challenge.realm}")
  "0x#{digest.unpack1("H*")}"
end

.checksum_address(address) ⇒ Object

Raises:

  • (ArgumentError)


26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/mpp/methods/evm/authorization.rb', line 26

def checksum_address(address)
  raw = address.to_s
  hex = raw.delete_prefix("0x")
  raise ArgumentError, "invalid address: #{address}" unless hex.match?(/\A[a-fA-F0-9]{40}\z/)

  begin
    hash = keccak256(hex.downcase).unpack1("H*")
  rescue LoadError
    return raw.start_with?("0x") ? raw : "0x#{hex}"
  end

  checksummed = hex.downcase.chars.each_with_index.map { |char, index|
    (char.match?(/[a-f]/) && hash[index].to_i(16) >= 8) ? char.upcase : char
  }.join
  "0x#{checksummed}"
end

.domain_separator(name:, version:, chain_id:, verifying_contract:) ⇒ Object



76
77
78
79
80
81
82
83
84
85
86
# File 'lib/mpp/methods/evm/authorization.rb', line 76

def domain_separator(name:, version:, chain_id:, verifying_contract:)
  keccak256(
    abi_encode(
      keccak256(DOMAIN_TYPE_HASH),
      keccak256(name),
      keccak256(version),
      uint256(chain_id),
      address_word(verifying_contract)
    )
  )
end

.keccak256(data) ⇒ Object



14
15
16
17
18
19
# File 'lib/mpp/methods/evm/authorization.rb', line 14

def keccak256(data)
  Kernel.require "eth"
  Eth::Util.keccak256(data)
rescue Gem::LoadError
  raise LoadError, "eth gem is not available"
end

.recover(authorization:, chain_id:, currency:, payload:) ⇒ Object



61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/mpp/methods/evm/authorization.rb', line 61

def recover(authorization:, chain_id:, currency:, payload:)
  hash = signing_hash(
    authorization: authorization,
    chain_id: chain_id,
    currency: currency,
    from: payload["from"],
    to: payload["to"],
    value: payload["value"],
    valid_after: payload["validAfter"],
    valid_before: payload["validBefore"],
    nonce: payload["nonce"]
  )
  recover_address(hash, payload["signature"])
end

.recover_address(hash, signature) ⇒ Object



127
128
129
130
131
132
133
134
135
136
# File 'lib/mpp/methods/evm/authorization.rb', line 127

def recover_address(hash, signature)
  Kernel.require "eth"

  sig = signature.to_s
  sig = "0x#{sig}" unless sig.start_with?("0x")
  recovered_key = Eth::Signature.recover(hash, sig)
  Eth::Util.public_key_to_address(recovered_key).to_s
rescue
  nil
end

.signing_hash(authorization:, chain_id:, currency:, from:, to:, value:, valid_after:, valid_before:, nonce:) ⇒ Object



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/mpp/methods/evm/authorization.rb', line 43

def signing_hash(authorization:, chain_id:, currency:, from:, to:, value:, valid_after:, valid_before:, nonce:)
  domain = domain_separator(
    name: authorization["name"] || authorization[:name],
    version: authorization["version"] || authorization[:version],
    chain_id: chain_id,
    verifying_contract: currency
  )
  struct = struct_hash(
    from: from,
    to: to,
    value: value,
    valid_after: valid_after,
    valid_before: valid_before,
    nonce: nonce
  )
  keccak256("\x19\x01".b + domain + struct)
end

.struct_hash(from:, to:, value:, valid_after:, valid_before:, nonce:) ⇒ Object



88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/mpp/methods/evm/authorization.rb', line 88

def struct_hash(from:, to:, value:, valid_after:, valid_before:, nonce:)
  keccak256(
    abi_encode(
      keccak256(TRANSFER_TYPE_HASH),
      address_word(from),
      address_word(to),
      uint256(value),
      uint256(valid_after),
      uint256(valid_before),
      bytes32_word(nonce)
    )
  )
end

.uint256(value) ⇒ Object

Raises:

  • (ArgumentError)


106
107
108
109
110
111
# File 'lib/mpp/methods/evm/authorization.rb', line 106

def uint256(value)
  value = Integer(value)
  raise ArgumentError, "uint256 out of range" if value.negative? || value >= (1 << 256)

  [value.to_s(16).rjust(64, "0")].pack("H*")
end