Skip to content

Conversation

@kuksag
Copy link

@kuksag kuksag commented Jan 14, 2026

Summary by CodeRabbit

  • New Features

    • ADR-36 payload support added — message payload type, signing, verification, and integration into multi-payload handling.
    • Extraction/deserialization of application data from ADR-36 messages enabled.
  • Tests

    • Unit tests added for ADR-36 hashing, successful verification, and negative verification cases.
  • Chores

    • New workspace package added and wired into the workspace and core dependencies; payload module exposed.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 14, 2026

📝 Walkthrough

Walkthrough

Adds a new workspace crate adr36 implementing ADR‑36 payload and signed‑payload types; integrates it into the workspace and core by exporting a new payload module, adding a MultiPayload::Adr36 variant, and implementing hashing, signature verification, and JSON extraction with unit tests.

Changes

Cohort / File(s) Summary
Workspace config
Cargo.toml, core/Cargo.toml
Added workspace member adr36 and added defuse-adr36 = { workspace = true } as a dependency in core.
ADR‑36 crate manifest
adr36/Cargo.toml
New crate manifest (defuse-adr36) with package metadata, dependencies (defuse-crypto, near-sdk), dev-deps, and workspace lints.
ADR‑36 implementation
adr36/src/lib.rs
New Adr36Payload, SignedAdr36Payload, and Algorithm enum; Amino JSON prehash (SHA‑256 / Keccak256), Payload / SignedPayload impls, Secp256k1 serde wiring, and unit tests.
Core payload export
core/src/payload/mod.rs
Added pub mod adr36; to expose the new payload submodule.
Core payload extraction
core/src/payload/adr36.rs
Implemented ExtractDefusePayload<T> for Adr36Payload and SignedAdr36Payload (deserializes JSON from message).
MultiPayload integration
core/src/payload/multi.rs
Added Adr36(SignedAdr36Payload) variant; updated hash(), verify(), extract_defuse_payload() match arms and imports to handle Adr36.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Adr36 as adr36::lib
  participant Signer
  participant Core
  participant Secp as Secp256k1

  Client->>Adr36: create Adr36Payload(message, signer, algo)
  Adr36->>Adr36: prehash() -> Amino JSON -> SHA-256 / Keccak256
  Client->>Signer: request signature(prehash)
  Signer->>Client: return signature
  Client->>Core: submit SignedAdr36Payload (MultiPayload::Adr36)
  Core->>Secp: verify(signature, payload.hash())
  Secp-->>Core: public key / None
  Core->>Client: verification result and extracted JSON
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • fusede
  • mitinarseny
  • pityjllk

Poem

🐰 A crate hopped in with a squeak and a spin,
I hashed the message, tucked its bits in,
Signatures checked, a pubkey unfurled,
JSON popped open, a curious world,
Hooray — code hopped home with a grin! 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title 'Add ADR-36 signatures support' accurately and concisely summarizes the main change across all modified files, which introduce comprehensive ADR-36 signature support including new payload types, implementations, and integration into the multi-payload system.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@adr36/Cargo.toml`:
- Around line 1-19: The Cargo.toml declares an unused dependency
defuse-near-utils; remove the defuse-near-utils workspace entry from the
[dependencies] section (delete the line containing "defuse-near-utils.workspace
= true") so the crate no longer depends on it and then run cargo check to
confirm no missing imports.

In `@adr36/src/lib.rs`:
- Around line 118-119: Rename the test function symbol test_expected_sh256_hash
to the correctly spelled test_expected_sha256_hash; update the #[test] function
declaration for test_expected_sh256_hash to fn test_expected_sha256_hash() and
adjust any references or calls to that test name (if any) so they match the new
identifier to ensure the test runs and naming is consistent.
🧹 Nitpick comments (2)
adr36/src/lib.rs (2)

93-101: Consider extracting shared fix_v_in_signature helper.

This function is noted as copy-paste from erc191/lib.rs. Consider extracting it to a shared location in defuse-crypto to avoid duplication and ensure consistent behavior across payload types.


141-153: Consider strengthening the negative test assertion.

The current test uses assert_ne! which only verifies the returned public key doesn't match REFERENCE_PUBKEY. If the goal is to verify that a wrong signature fails verification entirely, consider also checking that verify() returns None or a different public key.

More explicit negative test
     #[test]
     fn test_reference_signature_verification_fails() {
         let payload = Adr36Payload::new(
             REFERENCE_MESSAGE.to_string(),
             REFERENCE_SECP256K1_SIGNER.to_string(),
         );
         let signature = fix_v_in_signature(WRONG_REFERENCE_SIGNATURE);
 
-        assert_ne!(
-            SignedAdr36Payload { payload, signature }.verify(),
-            Some(REFERENCE_PUBKEY)
-        );
+        let result = SignedAdr36Payload { payload, signature }.verify();
+        // Verify either verification fails entirely (None) or recovers a different key
+        assert!(
+            result.is_none() || result != Some(REFERENCE_PUBKEY),
+            "Expected verification to fail or recover different pubkey"
+        );
     }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 057dc1e and 3efb3dc.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • Cargo.toml
  • adr36/Cargo.toml
  • adr36/src/lib.rs
  • core/Cargo.toml
  • core/src/payload/adr36.rs
  • core/src/payload/mod.rs
  • core/src/payload/multi.rs
🧰 Additional context used
🧬 Code graph analysis (2)
core/src/payload/adr36.rs (2)
core/src/payload/mod.rs (2)
  • extract_defuse_payload (43-43)
  • extract_defuse_payload (50-52)
core/src/payload/multi.rs (1)
  • extract_defuse_payload (106-117)
adr36/src/lib.rs (2)
core/src/payload/multi.rs (2)
  • hash (67-78)
  • verify (85-96)
crypto/src/signature.rs (1)
  • sig (159-159)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Check
  • GitHub Check: Shared / Static Analysis (intents-poa-factory, contract, near, poa-factory, 1, ., 1.18, false, --filter-pa...
  • GitHub Check: Shared / Static Analysis (intents-poa-token, contract, near, poa-token, 1, ., 1.18, false, --filter-paths ...
  • GitHub Check: Shared / Static Analysis (intents-contract, contract, near, defuse, 1, ., 1.18, false, --filter-paths "nod...
  • GitHub Check: Shared security analysis / Static Analysis (intents-contract, contract, near, defuse, 1, ., 1.18, false, --filter-paths "nod...
  • GitHub Check: Shared security analysis / Static Analysis (intents-poa-token, contract, near, poa-token, 1, ., 1.18, false, --filter-paths ...
  • GitHub Check: Shared security analysis / Static Analysis (intents-poa-factory, contract, near, poa-factory, 1, ., 1.18, false, --filter-pa...
  • GitHub Check: Security Audit - report
🔇 Additional comments (15)
Cargo.toml (1)

5-5: LGTM!

The workspace member and path dependency are correctly added following the existing alphabetical ordering and patterns.

Also applies to: 49-49

core/Cargo.toml (1)

10-10: LGTM!

The dependency is correctly added using the workspace pattern, consistent with other payload-related dependencies.

core/src/payload/mod.rs (1)

1-1: LGTM!

Module declaration correctly placed in alphabetical order before erc191.

core/src/payload/multi.rs (3)

5-5: LGTM!

Import correctly added for the new ADR-36 payload type.


76-76: LGTM!

The Adr36 variant is correctly integrated into all three trait implementations:

  • Payload::hash() delegates to the inner payload's hash
  • SignedPayload::verify() maps to PublicKey::Secp256k1 (correct for Cosmos SDK which uses secp256k1)
  • ExtractDefusePayload::extract_defuse_payload() delegates extraction

The pattern is consistent with other payload types.

Also applies to: 94-94, 115-115


54-58: LGTM!

The Adr36 variant is well-documented with references to the ADR-36 specification and Keplr wallet implementation docs. The ADR-36 prehash construction (base64-encoded message within a standard JSON structure), signature verification using secp256k1, and comprehensive test coverage with reference vectors from the cosmos-sdk all confirm the implementation is correct and spec-compliant.

core/src/payload/adr36.rs (2)

5-15: LGTM!

The delegation pattern for SignedAdr36Payload is consistent with how other signed payload types work in the codebase. The #[inline] hint is appropriate for this thin wrapper.


17-26: LGTM!

The JSON parsing from self.message is the correct approach for ADR-36 payloads, where the message field contains the serialized DefusePayload. This aligns with the prehash() implementation in adr36/src/lib.rs that base64-encodes the message.

adr36/src/lib.rs (7)

7-20: LGTM!

Good documentation with links to the ADR-36 specification and Keplr usage docs. The Bech32 address format explanation for the signer field is helpful for maintainability.


22-26: LGTM!

The const fn constructor is appropriate here. Input validation (e.g., Bech32 format) can reasonably be deferred to signature verification time.


54-59: LGTM!

The SHA-256 hash of the prehashed bytes is correct per ADR-36 specification.


61-68: LGTM!

The SignedAdr36Payload structure correctly wraps the payload with a Secp256k1 signature, using the AsCurve serde wrapper for proper serialization.


70-75: LGTM!

Correctly delegates to the inner payload's hash, which is the standard pattern for signed payloads.


77-84: LGTM!

The SignedPayload implementation correctly uses Secp256k1 verification. The integration with MultiPayload in core/src/payload/multi.rs (line 94) properly maps the result to PublicKey::Secp256k1.


28-51: No action required. The JSON key ordering is correct.

The serde_json::json! macro uses BTreeMap by default, which automatically sorts keys in lexicographic order. This is the correct behavior for ADR-36's canonical JSON serialization requirement. The keys in the prehash JSON are already properly ordered: account_number, chain_id, fee, memo, msgs, sequence. No preserve_order feature is needed; the default behavior already produces deterministic, sorted output for correct signature verification.

Likely an incorrect or invalid review comment.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

pityjllk
pityjllk previously approved these changes Jan 15, 2026
Copy link
Collaborator

@pityjllk pityjllk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

Copy link
Collaborator

@mitinarseny mitinarseny left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kuksag thanks for the PR! Overall, looks good to me, see just a minor comment below

{
"type": "sign/MsgSignData",
"value": {
"data": STANDARD.encode(self.message.as_bytes()),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to docs:

It's applications developers decision how Data should be treated, by treated we mean the serialization and deserialization process and the Object Data should represent.

Since we already represent self.message as String, does it make sense to avoid base64 encoding here and embed message as-is?

Copy link
Author

@kuksag kuksag Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. I'm gonna say I've overlooked that, because I was referencing implementations of the most used wallets on Cosmos SDK, and they wrap a message into base64:

To comply with the standard and to have something working in practise, we can verify the signature against both b64(message) (as of right now) and plaindata.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, ok, if that's a convention across ecosystem - I'm ok with leaving it in current state, i.e. always encode as base64

mitinarseny
mitinarseny previously approved these changes Jan 20, 2026
adr36/src/lib.rs Outdated
impl Payload for Adr36Payload {
#[inline]
fn hash(&self) -> CryptoHash {
env::sha256_array(self.prehash())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got a question - cosmos adr-36 allows compability with ethereum native wallets using EIP712 standart - https://github.com/chainapsis/keplr-wallet/blob/59b2e18122dc2ec3b12d3005fec709e4bcc885f8/packages/cosmos/src/adr-36/amino.ts#L150
type of algo is not part of signed msg - does it mean that we support only native cosmos signatures? If not may be include algo in payload?

Copy link
Author

@kuksag kuksag Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch.
We've discussed it internally and decided that if the chain is Ethermint-like, then they can simply sign an intent in evm style from e.g. Metamask.
Or, if the signer is inside Cosmos wallet, they still can sign an evm-style message, like this

Anyways, I've added support for algo selection, just for the sake of compatibility. In theory all known formats can be added, but I think adding support for the rest is too much for now.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@adr36/src/lib.rs`:
- Around line 110-116: The verify() implementation in SignedAdr36Payload
(function SignedAdr36Payload::verify) does not normalize Ethereum-style recovery
byte v (27/28), so production verification can fail for wallet signatures;
update verify() to normalize the signature by calling the existing
fix_v_in_signature(...) on self.signature (or a copy) before passing it to
Secp256k1::verify(&normalized_sig, &self.payload.hash(), &()), ensuring the same
v-normalization used in tests is applied in production.

Comment on lines +110 to +116
impl SignedPayload for SignedAdr36Payload {
type PublicKey = <Secp256k1 as Curve>::PublicKey;

#[inline]
fn verify(&self) -> Option<Self::PublicKey> {
Secp256k1::verify(&self.signature, &self.payload.hash(), &())
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, find the adr36 directory and examine the file
find . -type f -name "lib.rs" | grep -E "adr36|Adr36"

Repository: near/intents

Length of output: 74


🏁 Script executed:

# Read the file to see the context around lines 110-116 and 126-132
cat -n adr36/src/lib.rs | sed -n '100,145p'

Repository: near/intents

Length of output: 2104


🏁 Script executed:

# Check the entire file to understand the structure
wc -l adr36/src/lib.rs

Repository: near/intents

Length of output: 76


🏁 Script executed:

# Check where fix_v_in_signature is used
grep -n "fix_v_in_signature" adr36/src/lib.rs

Repository: near/intents

Length of output: 268


🏁 Script executed:

# See more of the test code to understand how it's used
cat -n adr36/src/lib.rs | sed -n '145,189p'

Repository: near/intents

Length of output: 1786


🏁 Script executed:

# Check if there are other files that might handle v normalization
rg "fix_v_in_signature|normalize.*v" --type rust

Repository: near/intents

Length of output: 1005


Normalize Ethereum-style recovery v before verification.

Tests normalize v using fix_v_in_signature() (lines 126-133) before calling verify(), but production verify() doesn't perform this normalization. Ethereum wallets emit v = 27/28, and without normalization in the production code, signatures from real wallets will fail verification even though test signatures work (because tests manually normalize before verification).

🛠️ Proposed fix
+fn normalize_v(mut sig: <Secp256k1 as Curve>::Signature) -> <Secp256k1 as Curve>::Signature {
+    // Ethereum-style signatures use v=27/28; normalize to 0/1
+    if sig[64] >= 27 {
+        sig[64] -= 27;
+    }
+    sig
+}
+
 impl SignedPayload for SignedAdr36Payload {
     type PublicKey = <Secp256k1 as Curve>::PublicKey;

     #[inline]
     fn verify(&self) -> Option<Self::PublicKey> {
-        Secp256k1::verify(&self.signature, &self.payload.hash(), &())
+        let sig = normalize_v(self.signature);
+        Secp256k1::verify(&sig, &self.payload.hash(), &())
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
impl SignedPayload for SignedAdr36Payload {
type PublicKey = <Secp256k1 as Curve>::PublicKey;
#[inline]
fn verify(&self) -> Option<Self::PublicKey> {
Secp256k1::verify(&self.signature, &self.payload.hash(), &())
}
fn normalize_v(mut sig: <Secp256k1 as Curve>::Signature) -> <Secp256k1 as Curve>::Signature {
// Ethereum-style signatures use v=27/28; normalize to 0/1
if sig[64] >= 27 {
sig[64] -= 27;
}
sig
}
impl SignedPayload for SignedAdr36Payload {
type PublicKey = <Secp256k1 as Curve>::PublicKey;
#[inline]
fn verify(&self) -> Option<Self::PublicKey> {
let sig = normalize_v(self.signature);
Secp256k1::verify(&sig, &self.payload.hash(), &())
}
}
🤖 Prompt for AI Agents
In `@adr36/src/lib.rs` around lines 110 - 116, The verify() implementation in
SignedAdr36Payload (function SignedAdr36Payload::verify) does not normalize
Ethereum-style recovery byte v (27/28), so production verification can fail for
wallet signatures; update verify() to normalize the signature by calling the
existing fix_v_in_signature(...) on self.signature (or a copy) before passing it
to Secp256k1::verify(&normalized_sig, &self.payload.hash(), &()), ensuring the
same v-normalization used in tests is applied in production.

@kuksag
Copy link
Author

kuksag commented Jan 23, 2026

Also I will share with you our plan on how we will integrate it on our side.
To execute intent, the contract has to verify that public key is associated with a signer:

if !self.state.has_public_key(&signer_id, &public_key) {

In case when there's no public keys associated with the signer_id, the contract tries implicitly converting public key to the signer_id. Addresses on Cosmos SDK has a format [chain prefix] || [separator] || [derivation from public key]. To avoid dealing with the zoo of possible options, we will supply the signer_id of the intent message as 0x[evm-style-public-key-derivation], as we can control this from the frontend. Then we fall into this pattern arm and succeed a check:

Self::Secp256k1(pk) => {

Note: signer field in Adr36Payload stays unchanged, as it doesn't play any role in intent execution.

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

Successfully merging this pull request may close these issues.

4 participants