A delegate call lets a contract execute code from another contract while keeping its own context. That means storage, msg.sender, and msg.value all remain those of the calling contract—so state changes affect the caller, and the original sender/value are preserved.
In Stylus, you can delegate-call in two ways:
delegate_call function — unsafe, bytes-in/bytes-out.RawCall with new_delegate() — unsafe, fluent builder for advanced control.⚠️ Safety: Delegate calls are powerful and dangerous. The callee can arbitrarily mutate your storage or spend ETH. Even though caches are cleared for safety, this does not prevent harmful behavior; use only with trusted code.
delegate_call functiondelegate_call is a low-level, unsafe operation (similar to call/static_call) that requires you to supply the VM handle and a mutating call context. You pass the target address and raw calldata, receive raw return bytes, and handle errors yourself.
1// low-level delegate call (unsafe)
2pub fn low_level_delegate_call(
3 &mut self,
4 calldata: Bytes,
5 target: Address,
6) -> Result<Vec<u8>, DelegateCallErrors> {
7 unsafe {
8 // Build a mutating context; delegate calls always mutate the caller’s storage
9 let config = Call::new_mutating(self);
10
11 // Perform the delegate call: VM handle + config + target + calldata
12 let result = delegate_call(self.vm(), config, target, &calldata)
13 .map_err(|_| DelegateCallErrors::DelegateCallFailed(DelegateCallFailed {}))?;
14
15 Ok(result)
16 }
17}1// low-level delegate call (unsafe)
2pub fn low_level_delegate_call(
3 &mut self,
4 calldata: Bytes,
5 target: Address,
6) -> Result<Vec<u8>, DelegateCallErrors> {
7 unsafe {
8 // Build a mutating context; delegate calls always mutate the caller’s storage
9 let config = Call::new_mutating(self);
10
11 // Perform the delegate call: VM handle + config + target + calldata
12 let result = delegate_call(self.vm(), config, target, &calldata)
13 .map_err(|_| DelegateCallErrors::DelegateCallFailed(DelegateCallFailed {}))?;
14
15 Ok(result)
16 }
17}Why unsafe? You’re executing untrusted code in your storage context; the callee can change anything. Although caches are cleared, that doesn’t stop malicious behavior.
RawCall with new_delegate()RawCall provides a fluent, bytes-in/bytes-out builder for unsafe calls. Use it when you need granular control (gas limits, return-data windows, cache flushing).
1pub fn raw_delegate_call(
2 &mut self,
3 calldata: Vec<u8>,
4 target: Address,
5) -> Result<Vec<u8>, Vec<u8>> {
6 let data = unsafe {
7 RawCall::new_delegate(self.vm()) // configure a delegate call bound to this VM
8 .gas(2100) // supply 2100 gas
9 .limit_return_data(0, 32) // only read the first 32 bytes back
10 .flush_storage_cache() // flush storage cache before the call
11 .call(target, &calldata)? // execute
12 };
13 Ok(data)
14}1pub fn raw_delegate_call(
2 &mut self,
3 calldata: Vec<u8>,
4 target: Address,
5) -> Result<Vec<u8>, Vec<u8>> {
6 let data = unsafe {
7 RawCall::new_delegate(self.vm()) // configure a delegate call bound to this VM
8 .gas(2100) // supply 2100 gas
9 .limit_return_data(0, 32) // only read the first 32 bytes back
10 .flush_storage_cache() // flush storage cache before the call
11 .call(target, &calldata)? // execute
12 };
13 Ok(data)
14}
RawCallexposes helpers like.gas(...),.limit_return_data(...), and cache controls. Use it sparingly: it’s still unsafe and untyped. (SeeRawCalldocs for the delegate builder and options.) ([Docs.rs][3])
msg.sender, msg.value) and thus can mutate the caller’s state.delegate_call: unsafe, requires VM handle + mutating Call context; returns raw bytes; you must handle errors.RawCall::new_delegate(...): unsafe, fluent builder with gas/return-data/cache controls.1Loading...1Loading...1Loading...1Loading...