Vyper struct ABI encoding vs Solidity caller interoperability
0 reputation · 03 Jan 2025, 07:27 UTC
0 reputation · 03 Jan 2025, 07:27 UTC
I need a Vyper external function to return a struct that a Solidity caller decodes with abi.decode on a live chain. Tests may pass locally when both contracts are compiled with the same Vyper version, but production uses a Solidity consumer.
Vyper 0.2.x and 0.3.x encode structs in a tightly packed format that omits padding between elements. Solidity's default ABI inserts padding to align dynamic types. Returning a uint128 followed by an address can therefore shift subsequent fields in a Solidity decoder, producing incorrect data or a revert. It is unclear whether Vyper will adopt Solidity-compatible struct alignment, and a future compiler release could change the layout without a major version bump.
34775 reputation · 03 Jan 2025, 10:34 UTC
1. No, Vyper’s current external‑return struct encoding is not Solidity‑compatible. Vyper packs struct members tightly (no 32‑byte word padding), while Solidity’s ABI expects each member to start on a 32‑byte boundary.
2. There is no compiler flag to make Vyper emit Solidity‑aligned tuples. The packed layout is intentional and will remain until a breaking change is introduced in a major release.
3. Production contracts should pin Vyper to a specific patch version (e.g., 0.3.10) and run an integration test against a Solidity consumer on a forked mainnet.
Vyper’s ABI follows the packed‑tuple rule. For a struct like
struct Data { uint128 a; address b; uint256 c; }
Vyper returns 48 bytes (16 + 20 + 32). Solidity’s abi.decode however expects 96 bytes because each field is padded to a full 32‑byte word. The decoder will therefore read the wrong values or revert if the data is too short.
Because the layout is not governed by semantic‑versioning guarantees, a minor or patch release could alter the packing rules without a major bump. The Vyper team has not announced any plan to align with Solidity’s tuple encoding.
cast call or an eth_call RPC. Confirm the byte length equals the sum of member sizes (packed).bytes[96] and pack the fields manually using concat and convert:
def get() -> bytes[96]:
return concat(convert(a, bytes[16]), convert(b, bytes[20]), convert(c, bytes[32]))
abi.decode on the 96‑byte array, or decode manually with assembly to avoid the padding mismatch.Pin the Vyper compiler to a specific patch (e.g., 0.3.10) in your pyproject.toml or requirements.txt. Add a comment explaining that the packed‑tuple behavior is intentional and may change in a future minor release. Continue to run the integration test after every patch upgrade to catch silent drift.
If your Solidity consumer already performs padding (e.g., by using abi.encode on the struct), let us know so we can adjust the workaround accordingly.
Use comments to ask for clarification. Post a solution as an answer.
5,940 reputation · 03 Jan 2025, 17:29 UTC
Before relying on cross-compiler struct decoding, verify the exact byte layout for your pinned Vyper version. Solidity's abi.decode assigns each struct member a 32-byte slot, while Vyper's encoder may pack adjacent smaller fields without that padding in some versions. A uint128 followed by an address can therefore shift every later field in the Solidity decoder.
There is no documented Vyper compiler flag to enable Solidity-compatible struct alignment in 0.2.x or 0.3.x. A practical check is to add explicit padding members in the Vyper struct, or handle the offset manually on the Solidity side, then confirm both sides produce identical values.
Run vyper --abi against the exact compiler commit and run a local round-trip test: call the Vyper function, capture the return bytes, and decode them in a Solidity test contract. Pin the commit, not just the minor version, because encoding behavior can drift between patch releases.