[PLEX-2731] - Cre forwarder#87
Conversation
Soroban Contract Test Coverage93.21% line coverage — 18033 / 19347 lines hit
Per-Contract Breakdown
Full file-level coverage report |
Integration Test Coverage (excl. Token Pool) |
Integration Test Coverage (Token Pool) |
There was a problem hiding this comment.
Pull request overview
Adds a new Soroban smart contract crate (contracts/cre) implementing a “KeystoneForwarder” with ownership, forwarder registry, config management, signature verification, receiver dispatch, and transmission tracking for CRE report forwarding.
Changes:
- Introduces the KeystoneForwarder contract, storage schema, and event types.
- Adds an extensive Rust test suite covering initialization, config lifecycle, signature validation, replay rules, and receiver behavior.
- Registers the new crate in the workspace (and lockfile) and adds a local Makefile for build/test.
Reviewed changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| contracts/cre/src/lib.rs | Core contract implementation: ownership/initialization guards, config + forwarder registry, report validation + signature verification, receiver dispatch + transmission tracking |
| contracts/cre/src/types.rs | Contract storage/data model types (config, transmissions, storage keys, state enums) |
| contracts/cre/src/events.rs | Contract event definitions for forwarder/config/report processing |
| contracts/cre/src/test.rs | Comprehensive end-to-end and negative-path tests, plus crypto/test fixtures and mock receivers |
| contracts/cre/Cargo.toml | New crate manifest and dependencies/dev-dependencies |
| contracts/cre/Makefile | Local build/test/fmt targets for the new contract crate |
| Cargo.toml | Adds contracts/cre to the workspace members |
| Cargo.lock | Adds lock entries for the new crate and dev-deps |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| fn receiver_contract_id_bytes(env: &Env, receiver: &Address) -> Bytes { | ||
| match receiver.to_payload() { | ||
| Some(AddressPayload::ContractIdHash(hash)) => Bytes::from(hash), | ||
| _ => panic_with_error!(env, Error::InvalidReceiver), | ||
| } | ||
| } |
| if f == 0 { | ||
| panic_with_error!(&env, Error::FaultToleranceMustBePositive); | ||
| } | ||
| if signers.len() > MAX_ORACLES { | ||
| panic_with_error!(&env, Error::ExcessSigners); | ||
| } | ||
| // BFT bound: need ≥ 3f + 1 signers configured to tolerate f faulty. | ||
| if signers.len() < f * 3 + 1 { | ||
| panic_with_error!(&env, Error::InsufficientSigners); | ||
| } |
| soroban-sdk = { workspace = true, features = ["testutils", "hazmat-address"] } | ||
| k256 = { version = "0.13", features = ["ecdsa"] } | ||
| sha3 = "0.10" | ||
| hex = "0.4" |
There was a problem hiding this comment.
nitpick: could be useful to move those to workspace deps so all contracts can use the same version, we'd need to do the same in CCIP contracts for things like sha3
| // Storage TTL constants (ledger counts; 1 ledger ≈ 5 s on Mainnet). | ||
| // TODO adjust | ||
| const BUMP_FOR_60_DAYS: u32 = 1_036_800; // ~60 days | ||
| const BUMP_AFTER_30_DAYS: u32 = 518_400; // ~30 days |
There was a problem hiding this comment.
I recommend making this configurable using instance storage
| _ => unreachable!("unexpected CCIPError variant from Ownable/Initializable"), | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
nitpick: moving this into its own file could be helpful
| <KeystoneForwarder as Ownable>::require_owner(env)?; | ||
| env.storage() | ||
| .instance() | ||
| .extend_ttl(BUMP_AFTER_30_DAYS, BUMP_FOR_60_DAYS); |
There was a problem hiding this comment.
instance storage does not require extending the TTL as it lives as long as the contract itself
| fn ensure_initialized(env: &Env) { | ||
| <KeystoneForwarder as Initializable>::require_initialized(env) | ||
| .unwrap_or_else(|_| panic_with_error!(env, Error::Uninitialized)); | ||
| } |
There was a problem hiding this comment.
This helper can be made into a one liner with the following change:
| fn ensure_initialized(env: &Env) { | |
| <KeystoneForwarder as Initializable>::require_initialized(env) | |
| .unwrap_or_else(|_| panic_with_error!(env, Error::Uninitialized)); | |
| } | |
| fn ensure_initialized(env: &Env) -> Result<(), Error> { | |
| <KeystoneForwarder as Initializable>::require_initialized(env).map_err(|e| e.into()) | |
| } |
Subsequent uses of the method could be as follows:
fn assert_owner(env: &Env) -> Result<(), Error> {
ensure_initialized(env)?; // the `?` would take care of the panic
//....
}| metadata: raw_report.slice(FORWARDER_METADATA_LENGTH..METADATA_LENGTH), | ||
| payload: raw_report.slice(METADATA_LENGTH..raw_report.len()), | ||
| } | ||
| } |
There was a problem hiding this comment.
Could be cleaner to move this into an impl of the ParsedReport type which you'd call with something like:
ParsedReport::From(...)There was a problem hiding this comment.
That impl would then be extended to include the config_id() method as well
|
|
||
| // ───────────────────────────────────────────────────────────────────────────── | ||
| // Signature verification | ||
| // ───────────────────────────────────────────────────────────────────────────── |
There was a problem hiding this comment.
There's a trait in common that might be helpful here called SignatureQuorum, I'm not sure if it's exactly what you need but could save you from writing the signature verification code here as well
|
Code coverage report:
|
No description provided.