Skip to content

feat: wormhole receiver contract for stylus #2774

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

Open
wants to merge 14 commits into
base: main
Choose a base branch
from

Conversation

ayushboss
Copy link
Contributor

Summary

Implementing a Wormhole receiver contract for the stylus framework. The receiver currently handles parsing + verifying VAAs and will also be able to handle guardian set management.

Rationale

A Wormhole receiver is necessary for ultimately implementing a Pyth receiver in Stylus.

How has this been tested?

  • Current tests cover my changes
  • Added new tests
  • Manually tested the code

I'm building a test suite for the stylus contract through normal cargo tests. At the moment those tests are focused on VAA verification, more tests will follow for guardian set management soon. I'll be expanding those tests and also running more complete stylus tests once I have everything loaded.

@ayushboss ayushboss requested a review from a team as a code owner June 11, 2025 03:30
Copy link

vercel bot commented Jun 11, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
api-reference ✅ Ready (Inspect) Visit Preview 💬 Add feedback Jun 12, 2025 10:30pm
component-library ✅ Ready (Inspect) Visit Preview 💬 Add feedback Jun 12, 2025 10:30pm
developer-hub ✅ Ready (Inspect) Visit Preview 💬 Add feedback Jun 12, 2025 10:30pm
entropy-debugger ✅ Ready (Inspect) Visit Preview 💬 Add feedback Jun 12, 2025 10:30pm
entropy-explorer ✅ Ready (Inspect) Visit Preview 💬 Add feedback Jun 12, 2025 10:30pm
insights ✅ Ready (Inspect) Visit Preview 💬 Add feedback Jun 12, 2025 10:30pm
proposals ✅ Ready (Inspect) Visit Preview 💬 Add feedback Jun 12, 2025 10:30pm
staking ✅ Ready (Inspect) Visit Preview 💬 Add feedback Jun 12, 2025 10:30pm

Copy link
Contributor

@tejasbadadare tejasbadadare left a comment

Choose a reason for hiding this comment

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

Great work! Lots of comments, but most are minor and organizational. Do all the tests pass?

@@ -0,0 +1,9 @@
[package]
name = "pyth-contract"
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we use a more specific name here like "pyth-stylus-receiver"?

Copy link
Contributor

Choose a reason for hiding this comment

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

on second thought, we can also just remove this crate until we're ready to implement it to keep this PR atomic

resolver = "2"

[workspace.package]
edition = "2021"
Copy link
Contributor

Choose a reason for hiding this comment

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

If we're specifying channel = "1.87.0" in rust-toolchain.toml, we can bump edition to "2024".

Copy link
Contributor

Choose a reason for hiding this comment

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

move this to the workspace level?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We need this file here for stylus to be able to build

Copy link
Contributor

Choose a reason for hiding this comment

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

Let's remove the placeholder files and dirs until we need them (src/, sdk/, etc)

Copy link
Contributor

Choose a reason for hiding this comment

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

are these licenses necessary to include here? we have a blanket LICENSE (apache 2.0) at the repo root

Comment on lines +548 to +549
let hash = Keccak256::digest(&pubkey_uncompressed[1..]); // Skip 0x04
let address_bytes: [u8; 20] = hash[12..].try_into().unwrap(); // Last 20 bytes
Copy link
Contributor

Choose a reason for hiding this comment

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

why skip and slice these specific bytes?

]
}

fn current_guardians() -> Vec<Address> {
Copy link
Contributor

@tejasbadadare tejasbadadare Jun 11, 2025

Choose a reason for hiding this comment

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

these will change over time, and shouldn't be functionally different than the other guardian_set fixtures you've got here -- would recommend renaming it mock_guardian_set13.

also in general i'd recommend against calling certain tests "real" (are those tests considered better/more realistic than the non-real ones?) all fixtures should ideally be representative of real world inputs.

assert_eq!(result.signatures.len(), 13)
}

#[motsu::test]
Copy link
Contributor

Choose a reason for hiding this comment

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

what does motsu give us? have just seen cargo test used in our other projects

Copy link
Contributor Author

Choose a reason for hiding this comment

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

motsu is a package that mocks up the requirements of the stylus environment while doing unit tests

Comment on lines 817 to 827
#[motsu::test]
fn test_verify_vm_invalid_guardian_index() {
let contract = deploy_with_test_guardian();
let signatures = vec![
create_guardian_signature(5),
];
let vm = create_test_vm(0, signatures);

let result = contract.verify_vm(&vm);
assert!(matches!(result, Err(WormholeError::InvalidGuardianIndex)));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

can we also test this with multiple guardian sets loaded into storage?

}

#[motsu::test]
fn test_multiple_guardian_sets() {
Copy link
Contributor

Choose a reason for hiding this comment

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

great test coverage overall!!

Copy link
Collaborator

@ali-behjati ali-behjati left a comment

Choose a reason for hiding this comment

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

I'll review the rest tomorrow.

Copy link
Collaborator

Choose a reason for hiding this comment

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

is this needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I will need it for deployment, I am still figuring out exactly the best way to handle it though.

Comment on lines 8 to 12
// #[cfg(all(not(feature = "std"), not(test)))]
// #[panic_handler]
// fn panic(_info: &core::panic::PanicInfo) -> ! {
// loop {}
// }
Copy link
Collaborator

Choose a reason for hiding this comment

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

what's the context behind this :? shall we remove it?

Copy link
Collaborator

Choose a reason for hiding this comment

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

as an overal comment i recommend breaking this down to multiple modules.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Have separated the tests out into a different module on a different PR, will also think about the best way to branch these out more

VerfiyVMError,
}

impl From<WormholeError> for Vec<u8> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

maybe vec is what stylus accepts

pub hash: FixedBytes<32>,
}

#[derive(Debug, Clone)]
Copy link
Collaborator

Choose a reason for hiding this comment

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

not sure whether it's supported in stylus or not, but thiserror can help to give you Display for the error.

Comment on lines +105 to +107
current_guardian_set_index: StorageUint<256, 4>,
chain_id: StorageUint<256, 4>,
governance_chain_id: StorageUint<256, 4>,
Copy link
Collaborator

Choose a reason for hiding this comment

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

damn. this struct stores u64[4] inside instead of u8[256] to probably be more optimized. the 4 is a constant and is checked at compile time to make sure it's the right number for 256 bits.

}
}

pub fn parse_and_verify_vm(&self, encoded_vm: Vec<u8>) -> Result<Vec<u8>, Vec<u8>> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

this is the name in EVM implementations and I recommend sticking to the same name. ideally we want anyone from EVM to be able to interact with this contract just as it was the Pyth/Wormhole EVM contract.

Comment on lines 145 to 147
if !self.initialized.get() {
return Err(WormholeError::NotInitialized.into());
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

it's kind of strange that we need to check it everywhere

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is there a better way to do this? My other thought was to initialize the contract if we run into it when it's not initialized.

Comment on lines 264 to 269
let timestamp = u32::from_be_bytes([
encoded_vm[cursor],
encoded_vm[cursor + 1],
encoded_vm[cursor + 2],
encoded_vm[cursor + 3],
]);
Copy link
Collaborator

Choose a reason for hiding this comment

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

can't this be like let timestamp = u32::from_be_bytes(encoded_vm.get(cursor..cursor+4)).ok_or(WormholeError::InvalidVMFormat)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, didn't know about this until Tejas mentioned it to me, should make things cleaner.

cursor += 1;

let mut signature_bytes = [0u8; 65];
signature_bytes.copy_from_slice(&encoded_vm[cursor..cursor + 65]);
Copy link
Collaborator

Choose a reason for hiding this comment

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

generally instead of direct memory access you can use .get() that returns an Option and you can convert it to error and return if it's empty. this will be safer than manually checking size above and having a direct access.

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.

3 participants