Skip to content

Commit b18e018

Browse files
committed
[runtime]: fix ABI encoding mismatch for HFT and BandwidthPurchaseMsg
1 parent 3c95c0f commit b18e018

7 files changed

Lines changed: 320 additions & 8 deletions

File tree

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
pragma solidity ^0.8.17;
3+
4+
import {HyperFungibleToken} from "@hyperbridge/core/apps/HyperFungibleToken.sol";
5+
import {BandwidthPurchaseMsg, Tier, Withdrawal} from "../../src/apps/BandwidthManager.sol";
6+
import {Test} from "forge-std/Test.sol";
7+
8+
/// @dev Exposes encode/decode helpers for cross-language testing of app
9+
/// payloads (HFT Message, BandwidthPurchaseMsg, Tier[], Withdrawal).
10+
/// Each `encode*` returns what production Solidity calls (`abi.encode(x)`)
11+
/// and each `decode*` is what production Solidity expects on the
12+
/// receiving side (`abi.decode(data, (T))`).
13+
contract AbiAppsCodec {
14+
// ── HyperFungibleToken Message ────────────────────────────────────
15+
// Production: `sdk/packages/core/contracts/apps/HyperFungibleToken.sol:242,293,311`
16+
// `Message` is nested inside the `HyperFungibleToken` contract.
17+
18+
function encodeHftMessage(HyperFungibleToken.Message memory m)
19+
external
20+
pure
21+
returns (bytes memory)
22+
{
23+
return abi.encode(m);
24+
}
25+
26+
function decodeHftMessage(bytes memory data)
27+
external
28+
pure
29+
returns (HyperFungibleToken.Message memory)
30+
{
31+
return abi.decode(data, (HyperFungibleToken.Message));
32+
}
33+
34+
// ── BandwidthPurchaseMsg ─────────────────────────────────────────
35+
// Production: `evm/src/apps/BandwidthManager.sol:177` (outbound to pallet)
36+
37+
function encodeBandwidthPurchase(BandwidthPurchaseMsg memory m)
38+
external
39+
pure
40+
returns (bytes memory)
41+
{
42+
return abi.encode(m);
43+
}
44+
45+
function decodeBandwidthPurchase(bytes memory data)
46+
external
47+
pure
48+
returns (BandwidthPurchaseMsg memory)
49+
{
50+
return abi.decode(data, (BandwidthPurchaseMsg));
51+
}
52+
53+
// ── Tier[] (SetTiers governance payload) ─────────────────────────
54+
// Production: `evm/src/apps/BandwidthManager.sol:209` (inbound from pallet)
55+
56+
function encodeTiers(Tier[] memory tiers) external pure returns (bytes memory) {
57+
return abi.encode(tiers);
58+
}
59+
60+
function decodeTiers(bytes memory data) external pure returns (Tier[] memory) {
61+
return abi.decode(data, (Tier[]));
62+
}
63+
64+
// ── Withdrawal (Withdraw governance payload) ─────────────────────
65+
// Production: `evm/src/apps/BandwidthManager.sol:215` (inbound from pallet)
66+
67+
function encodeWithdrawal(Withdrawal memory w) external pure returns (bytes memory) {
68+
return abi.encode(w);
69+
}
70+
71+
function decodeWithdrawal(bytes memory data) external pure returns (Withdrawal memory) {
72+
return abi.decode(data, (Withdrawal));
73+
}
74+
}
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
//! Cross-language ABI parity tests for app-level payloads dispatched
2+
//! between Rust pallets and Solidity contracts:
3+
//!
4+
//! - HFT `Message` (`pallets/hyper-fungible-token` ↔ `apps/HyperFungibleToken.sol`)
5+
//! - `BandwidthPurchaseMsg` (`apps/BandwidthManager.sol` → `pallets/bandwidth`)
6+
//! - `Tier[]` (`pallets/bandwidth` → `apps/BandwidthManager.sol` SetTiers)
7+
//! - `Withdrawal` (`pallets/bandwidth` → `apps/BandwidthManager.sol` Withdraw)
8+
//!
9+
//! Each test mirrors the production encode/decode methods on both
10+
//! sides — the Rust assertions are intentionally what the pallets do
11+
//! today, so any divergence from Solidity's `abi.encode(struct)`
12+
//! surfaces here.
13+
14+
use super::utils::*;
15+
use alloy_primitives::{Address, Bytes, U256};
16+
use alloy_sol_types::{SolCall, SolValue};
17+
18+
alloy_sol_macro::sol! {
19+
// ── Mirror of production sol! types ─────────────────────────────
20+
21+
/// Matches `Message` in `sdk/packages/core/contracts/apps/HyperFungibleToken.sol:82-91`
22+
/// and the alloy declaration in `pallets/hyper-fungible-token/src/types.rs`.
23+
struct HftMessage {
24+
bytes from;
25+
bytes to;
26+
uint256 amount;
27+
bytes data;
28+
}
29+
30+
/// Matches `BandwidthPurchaseMsg` in `evm/src/apps/BandwidthManager.sol:32-41`
31+
/// and `BandwidthPurchaseMsgAbi` in `pallets/bandwidth/src/abi.rs:18-23`.
32+
struct BandwidthPurchaseMsg {
33+
bytes app;
34+
uint256 tier;
35+
uint256 months;
36+
bytes chain;
37+
}
38+
39+
/// Matches `Tier` in `evm/src/apps/BandwidthManager.sol:44-49`
40+
/// and `TierAbi` in `pallets/bandwidth/src/abi.rs:27-30`.
41+
struct Tier {
42+
uint256 tier;
43+
uint256 price;
44+
}
45+
46+
/// Matches `Withdrawal` in `evm/src/apps/BandwidthManager.sol:54-58`
47+
/// and `WithdrawalAbi` in `pallets/bandwidth/src/abi.rs:34-38`.
48+
struct Withdrawal {
49+
address token;
50+
address beneficiary;
51+
uint256 amount;
52+
}
53+
54+
// ── AbiAppsCodec function selectors ─────────────────────────────
55+
56+
function encodeHftMessage(HftMessage m) external pure returns (bytes);
57+
function decodeHftMessage(bytes data) external pure returns (HftMessage);
58+
59+
function encodeBandwidthPurchase(BandwidthPurchaseMsg m) external pure returns (bytes);
60+
function decodeBandwidthPurchase(bytes data) external pure returns (BandwidthPurchaseMsg);
61+
62+
function encodeTiers(Tier[] tiers) external pure returns (bytes);
63+
function decodeTiers(bytes data) external pure returns (Tier[]);
64+
65+
function encodeWithdrawal(Withdrawal w) external pure returns (bytes);
66+
function decodeWithdrawal(bytes data) external pure returns (Withdrawal);
67+
}
68+
69+
fn deploy_codec(env: &mut TestEnv) -> Address {
70+
let out_dir = env.evm_out_dir_public();
71+
env.deploy_named(&out_dir, "AbiAppsCodec")
72+
}
73+
74+
fn sample_hft_message() -> HftMessage {
75+
HftMessage {
76+
from: Bytes::from(hex::decode("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef").unwrap()),
77+
to: Bytes::from(hex::decode("cafebabecafebabecafebabecafebabecafebabe").unwrap()),
78+
amount: U256::from(1_000_000_000_000_000_000u128),
79+
data: Bytes::from(hex::decode("1234").unwrap()),
80+
}
81+
}
82+
83+
fn sample_bandwidth_purchase() -> BandwidthPurchaseMsg {
84+
BandwidthPurchaseMsg {
85+
app: Bytes::from(b"my-app".to_vec()),
86+
tier: U256::from(2u64),
87+
months: U256::from(3u64),
88+
chain: Bytes::from(b"EVM-8453".to_vec()),
89+
}
90+
}
91+
92+
fn sample_tiers() -> Vec<Tier> {
93+
vec![
94+
Tier { tier: U256::from(0u64), price: U256::from(10_000_000_000_000_000u128) },
95+
Tier { tier: U256::from(1u64), price: U256::from(50_000_000_000_000_000u128) },
96+
Tier { tier: U256::from(2u64), price: U256::from(250_000_000_000_000_000u128) },
97+
]
98+
}
99+
100+
fn sample_withdrawal() -> Withdrawal {
101+
Withdrawal {
102+
token: Address::repeat_byte(0xab),
103+
beneficiary: Address::repeat_byte(0xcd),
104+
amount: U256::from(123_456_789u64),
105+
}
106+
}
107+
108+
/// Encoding parity for the HFT `Message`. Mirrors the production path
109+
/// in `pallets/hyper-fungible-token/src/lib.rs:296` and `module.rs:59,215`,
110+
/// which use `abi_encode` / `abi_decode` to match Solidity's
111+
/// `abi.encode(struct)` / `abi.decode(data, (Message))`.
112+
#[test]
113+
fn test_hft_message_encoding_parity() {
114+
let mut env = TestEnv::new();
115+
let codec = deploy_codec(&mut env);
116+
let msg = sample_hft_message();
117+
118+
// Rust encodes — exactly as `pallets/hyper-fungible-token/src/lib.rs:296` does.
119+
let rust_encoded = HftMessage::abi_encode(&msg);
120+
121+
// Solidity encodes — exactly as `HyperFungibleToken.sol:242` does.
122+
let result = env.call(codec, encodeHftMessageCall { m: msg.clone() }.abi_encode());
123+
let sol_encoded = Bytes::abi_decode(&result).unwrap().to_vec();
124+
125+
assert_eq!(rust_encoded, sol_encoded, "HFT Message encoding mismatch");
126+
127+
// Solidity must be able to decode Rust-produced bytes.
128+
let _ = env.call(
129+
codec,
130+
decodeHftMessageCall { data: Bytes::from(rust_encoded.clone()) }.abi_encode(),
131+
);
132+
133+
// Rust must be able to decode Solidity-produced bytes — mirrors
134+
// `pallets/hyper-fungible-token/src/module.rs:59,215`.
135+
let decoded = HftMessage::abi_decode(&sol_encoded)
136+
.expect("Rust `abi_decode` failed on Solidity-encoded HFT Message");
137+
assert_eq!(decoded.from, msg.from);
138+
assert_eq!(decoded.to, msg.to);
139+
assert_eq!(decoded.amount, msg.amount);
140+
assert_eq!(decoded.data, msg.data);
141+
}
142+
143+
/// Encoding parity for `BandwidthPurchaseMsg`. Solidity encodes via
144+
/// `abi.encode(body)` (`BandwidthManager.sol:177`); Rust decodes via
145+
/// `abi_decode` (`pallets/bandwidth/src/abi.rs:64`). Mirror struct
146+
/// encoded with `abi_encode` to round-trip correctly.
147+
#[test]
148+
fn test_bandwidth_purchase_encoding_parity() {
149+
let mut env = TestEnv::new();
150+
let codec = deploy_codec(&mut env);
151+
let msg = sample_bandwidth_purchase();
152+
153+
// Rust encodes — exactly as `pallets/bandwidth/src/abi.rs:90` does.
154+
let rust_encoded = BandwidthPurchaseMsg::abi_encode(&msg);
155+
156+
// Solidity encodes — exactly as `BandwidthManager.sol:177` does.
157+
let result =
158+
env.call(codec, encodeBandwidthPurchaseCall { m: msg.clone() }.abi_encode());
159+
let sol_encoded = Bytes::abi_decode(&result).unwrap().to_vec();
160+
161+
assert_eq!(rust_encoded, sol_encoded, "BandwidthPurchaseMsg encoding mismatch");
162+
163+
// Rust must be able to decode Solidity-produced bytes — mirrors
164+
// `pallets/bandwidth/src/abi.rs:64`.
165+
let decoded = BandwidthPurchaseMsg::abi_decode(&sol_encoded)
166+
.expect("Rust `abi_decode` failed on Solidity-encoded BandwidthPurchaseMsg");
167+
assert_eq!(decoded.app, msg.app);
168+
assert_eq!(decoded.tier, msg.tier);
169+
assert_eq!(decoded.months, msg.months);
170+
assert_eq!(decoded.chain, msg.chain);
171+
}
172+
173+
/// Encoding parity for the `Tier[]` governance payload. Rust uses
174+
/// `rows.abi_encode_params()` (`pallets/bandwidth/src/lib.rs:317`);
175+
/// Solidity uses `abi.decode(body[1:], (Tier[]))`. For a dynamic
176+
/// array (non-tuple SolType), `abi_encode_params` wraps in a 1-tuple
177+
/// and matches `abi.encode(arr)`, so this should pass.
178+
#[test]
179+
fn test_tiers_encoding_parity() {
180+
let mut env = TestEnv::new();
181+
let codec = deploy_codec(&mut env);
182+
let tiers = sample_tiers();
183+
184+
// Rust encodes — exactly as `pallets/bandwidth/src/lib.rs:317` does.
185+
let rust_encoded = tiers.abi_encode_params();
186+
187+
// Solidity encodes — exactly as `abi.decode(body[1:], (Tier[]))`
188+
// expects on the receive side.
189+
let result = env.call(codec, encodeTiersCall { tiers: tiers.clone() }.abi_encode());
190+
let sol_encoded = Bytes::abi_decode(&result).unwrap().to_vec();
191+
192+
assert_eq!(rust_encoded, sol_encoded, "Tier[] encoding mismatch");
193+
194+
// Solidity must be able to decode Rust-produced bytes.
195+
let result = env.call(
196+
codec,
197+
decodeTiersCall { data: Bytes::from(rust_encoded.clone()) }.abi_encode(),
198+
);
199+
let decoded = <Vec<Tier> as SolValue>::abi_decode(&result).unwrap();
200+
assert_eq!(decoded.len(), tiers.len());
201+
for (a, b) in decoded.iter().zip(tiers.iter()) {
202+
assert_eq!(a.tier, b.tier);
203+
assert_eq!(a.price, b.price);
204+
}
205+
}
206+
207+
/// Encoding parity for the `Withdrawal` governance payload. Rust uses
208+
/// `payload.abi_encode_params()` (`pallets/bandwidth/src/lib.rs:346`);
209+
/// Solidity uses `abi.decode(body[1:], (Withdrawal))`. `Withdrawal`
210+
/// is all-static (`address`, `address`, `uint256`), so tuple-wrap vs
211+
/// fields-spread produce identical bytes — this should pass.
212+
#[test]
213+
fn test_withdrawal_encoding_parity() {
214+
let mut env = TestEnv::new();
215+
let codec = deploy_codec(&mut env);
216+
let w = sample_withdrawal();
217+
218+
// Rust encodes — exactly as `pallets/bandwidth/src/lib.rs:346` does.
219+
let rust_encoded = Withdrawal::abi_encode_params(&w);
220+
221+
// Solidity encodes — exactly as `abi.decode(body[1:], (Withdrawal))`
222+
// expects on the receive side.
223+
let result = env.call(codec, encodeWithdrawalCall { w: w.clone() }.abi_encode());
224+
let sol_encoded = Bytes::abi_decode(&result).unwrap().to_vec();
225+
226+
assert_eq!(rust_encoded, sol_encoded, "Withdrawal encoding mismatch");
227+
228+
// Solidity must be able to decode Rust-produced bytes.
229+
let result = env.call(
230+
codec,
231+
decodeWithdrawalCall { data: Bytes::from(rust_encoded) }.abi_encode(),
232+
);
233+
let decoded = Withdrawal::abi_decode(&result).unwrap();
234+
assert_eq!(decoded.token, w.token);
235+
assert_eq!(decoded.beneficiary, w.beneficiary);
236+
assert_eq!(decoded.amount, w.amount);
237+
}

evm/tests/rust/src/tests/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
mod utils;
22

33
mod abi_encode;
4+
mod abi_encode_apps;
45
mod ecdsa_beefy;
56
mod get_response;
67
mod get_timeout;

modules/pallets/bandwidth/src/abi.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ impl TryFrom<&[u8]> for PurchaseMessage {
6161
type Error = anyhow::Error;
6262

6363
fn try_from(body: &[u8]) -> Result<Self, Self::Error> {
64-
let abi = BandwidthPurchaseMsgAbi::abi_decode_params(body)
64+
let abi = BandwidthPurchaseMsgAbi::abi_decode(body)
6565
.map_err(|err| anyhow::anyhow!(format!("invalid bandwidth purchase ABI: {err:?}")))?;
6666

6767
let tier: u32 = abi.tier.try_into().map_err(|_| anyhow::anyhow!("tier exceeds u32"))?;
@@ -87,6 +87,6 @@ impl From<&PurchaseMessage> for Vec<u8> {
8787
months: alloy_primitives::U256::from(msg.months),
8888
chain: alloy_primitives::Bytes::from(msg.chain.to_string().into_bytes()),
8989
};
90-
BandwidthPurchaseMsgAbi::abi_encode_params(&abi)
90+
BandwidthPurchaseMsgAbi::abi_encode(&abi)
9191
}
9292
}

modules/pallets/hyper-fungible-token/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ pub mod pallet {
293293
from: PALLET_ID.to_bytes(),
294294
to: token_contract,
295295
timeout: params.timeout,
296-
body: Message::abi_encode_params(&token_message),
296+
body: Message::abi_encode(&token_message),
297297
};
298298

299299
let metadata = FeeMetadata { payer: who.clone(), fee: params.relayer_fee.into() };

modules/pallets/hyper-fungible-token/src/module.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ where
5656
.ok_or(HftError::UnknownSourceContract(source))?;
5757

5858
// Decode the Message
59-
let message = Message::abi_decode_params(&body).map_err(HftError::DecodeError)?;
59+
let message = Message::abi_decode(&body).map_err(HftError::DecodeError)?;
6060

6161
// Convert recipient bytes to substrate AccountId
6262
// If 32 bytes: use directly. If 20 bytes: left-pad with zeros.
@@ -212,7 +212,7 @@ where
212212
fn on_timeout(&self, request: Request) -> Result<Weight, anyhow::Error> {
213213
match request {
214214
Request::Post(PostRequest { body, to, dest, .. }) => {
215-
let message = Message::abi_decode_params(&body).map_err(HftError::DecodeError)?;
215+
let message = Message::abi_decode(&body).map_err(HftError::DecodeError)?;
216216

217217
// Refund the original sender
218218
let from_bytes = message.from.as_ref();

modules/pallets/testsuite/src/tests/pallet_hyper_fungible_token.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ fn should_receive_asset_correctly() {
8585
},
8686
data: alloy_primitives::Bytes::default(),
8787
};
88-
Message::abi_encode_params(&msg)
88+
Message::abi_encode(&msg)
8989
},
9090
};
9191

@@ -132,7 +132,7 @@ fn should_timeout_request_correctly() {
132132
},
133133
data: alloy_primitives::Bytes::default(),
134134
};
135-
Message::abi_encode_params(&msg)
135+
Message::abi_encode(&msg)
136136
},
137137
};
138138

@@ -305,7 +305,7 @@ fn should_receive_asset_with_calldata() {
305305
},
306306
data: alloy_primitives::Bytes::from(substrate_data.encode()),
307307
};
308-
Message::abi_encode_params(&msg)
308+
Message::abi_encode(&msg)
309309
},
310310
};
311311

0 commit comments

Comments
 (0)