An Arbitrum Stylus version implementation of Solidity TimeLock.
Here is the interface for TimeLock.
1interface ITimeLock {
2 function initialize() external;
3
4 function owner() external view returns (address);
5
6 function getTxId(address target, uint256 value, string calldata func, bytes calldata data, uint256 timestamp) external view returns (bytes32);
7
8 function deposit() external payable;
9
10 function queue(address target, uint256 value, string calldata func, bytes calldata data, uint256 timestamp) external;
11
12 function execute(address target, uint256 value, string calldata func, bytes calldata data, uint256 timestamp) external;
13
14 function cancel(address target, uint256 value, string calldata func, bytes calldata data, uint256 timestamp) external;
15
16 error AlreadyInitialized();
17
18 error NotOwnerError();
19
20 error AlreadyQueuedError(bytes32);
21
22 error TimestampNotInRangeError(uint256, uint256);
23
24 error NotQueuedError(bytes32);
25
26 error TimestampNotPassedError(uint256, uint256);
27
28 error TimestampExpiredError(uint256, uint256);
29
30 error TxFailedError();
31
32 event Queue(
33 bytes32 indexed txId,
34 address indexed target,
35 uint256 value,
36 string func,
37 bytes data,
38 uint256 timestamp
39 );
40
41 event Execute(
42 bytes32 indexed txId,
43 address indexed target,
44 uint256 value,
45 string func,
46 bytes data,
47 uint256 timestamp
48 );
49
50 event Cancel(bytes32 indexed txId);
51}
1interface ITimeLock {
2 function initialize() external;
3
4 function owner() external view returns (address);
5
6 function getTxId(address target, uint256 value, string calldata func, bytes calldata data, uint256 timestamp) external view returns (bytes32);
7
8 function deposit() external payable;
9
10 function queue(address target, uint256 value, string calldata func, bytes calldata data, uint256 timestamp) external;
11
12 function execute(address target, uint256 value, string calldata func, bytes calldata data, uint256 timestamp) external;
13
14 function cancel(address target, uint256 value, string calldata func, bytes calldata data, uint256 timestamp) external;
15
16 error AlreadyInitialized();
17
18 error NotOwnerError();
19
20 error AlreadyQueuedError(bytes32);
21
22 error TimestampNotInRangeError(uint256, uint256);
23
24 error NotQueuedError(bytes32);
25
26 error TimestampNotPassedError(uint256, uint256);
27
28 error TimestampExpiredError(uint256, uint256);
29
30 error TxFailedError();
31
32 event Queue(
33 bytes32 indexed txId,
34 address indexed target,
35 uint256 value,
36 string func,
37 bytes data,
38 uint256 timestamp
39 );
40
41 event Execute(
42 bytes32 indexed txId,
43 address indexed target,
44 uint256 value,
45 string func,
46 bytes data,
47 uint256 timestamp
48 );
49
50 event Cancel(bytes32 indexed txId);
51}
Example implementation of a Timelock contract written in Rust.
1// Allow `cargo stylus export-abi` to generate a main function if the "export-abi" feature is enabled.
2#![cfg_attr(not(feature = "export-abi"), no_main)]
3extern crate alloc;
4
5use sha3::{Digest, Keccak256};
6use alloc::string::String;
7use alloy_primitives::{Address, FixedBytes, U256};
8use alloy_sol_types::{sol, sol_data::{Address as SOLAddress, Bytes as SOLBytes, *}, SolType};
9// Import items from the SDK. The prelude contains common traits and macros.
10use stylus_sdk::{abi::Bytes, block, call::{call, Call}, evm, msg, prelude::*};
11
12// Define the types of the contract's storage.
13type TxIdHashType = (SOLAddress, Uint<256>, SOLBytes, SOLBytes, Uint<256>);
14
15sol!{
16 error AlreadyInitialized();
17 error NotOwnerError();
18 error AlreadyQueuedError(bytes32 txId);
19 error TimestampNotInRangeError(uint256 blockTimestamp, uint256 timestamp);
20 error NotQueuedError(bytes32 txId);
21 error TimestampNotPassedError(uint256 blockTimestamp, uint256 timestamp);
22 error TimestampExpiredError(uint256 blockTimestamp, uint256 expiresAt);
23 error TxFailedError();
24
25 event Queue(
26 bytes32 indexed txId,
27 address indexed target,
28 uint256 value,
29 string func,
30 bytes data,
31 uint256 timestamp
32 );
33 event Execute(
34 bytes32 indexed txId,
35 address indexed target,
36 uint256 value,
37 string func,
38 bytes data,
39 uint256 timestamp
40 );
41 event Cancel(bytes32 indexed txId);
42}
43
44// Define persistent storage using the Solidity ABI.
45// `TimeLock` will be the entrypoint for the contract.
46sol_storage! {
47 // Define the contract's storage.
48 #[entrypoint]
49 pub struct TimeLock {
50 address owner;
51 mapping(bytes32 => bool) queued;
52 }
53}
54
55// Error types for the TimeLock contract
56#[derive(SolidityError)]
57pub enum TimeLockError {
58 // Error for when the contract is already initialized.
59 AlreadyInitialized(AlreadyInitialized),
60 // Error for when the sender is not the owner
61 NotOwnerError(NotOwnerError),
62 // Error for when the transaction is already queued
63 AlreadyQueuedError(AlreadyQueuedError),
64 // Error for when the timestamp is not in the range
65 TimestampNotInRangeError(TimestampNotInRangeError),
66 // Error for when the transaction is not queued
67 NotQueuedError(NotQueuedError),
68 // Error for when the timestamp has not yet passed
69 TimestampNotPassedError(TimestampNotPassedError),
70 // Error for when the timestamp has expired
71 TimestampExpiredError(TimestampExpiredError),
72 // Error for when a transaction fails
73 TxFailedError(TxFailedError),
74}
75
76// Marks `TimeLock` as a contract with the specified external methods
77#[public]
78impl TimeLock {
79
80 // Minimum delay allowed for a transaction
81 pub const MIN_DELAY: u64 = 10;
82 // Maximum delay allowed for a transaction
83 pub const MAX_DELAY: u64 = 1000;
84 // Grace period after the maximum delay
85 pub const GRACE_PERIOD: u64 = 1000;
86
87 pub fn initialize(&mut self) -> Result<(), TimeLockError> {
88 if self.owner.get() != Address::default() {
89 return Err(TimeLockError::AlreadyInitialized(AlreadyInitialized{}))
90 }
91 self.owner.set(msg::sender());
92 Ok(())
93 }
94
95 pub fn owner(&self) -> Address {
96 self.owner.get()
97 }
98
99 // Function to generate a transaction ID
100 pub fn get_tx_id(
101 &self,
102 target: Address, // Target address for the transaction
103 value: U256, // Value to be transferred
104 func: String, // Function name to be called
105 data: Bytes, // Data to be passed to the function
106 timestamp: U256, // Timestamp for the transaction
107 ) -> FixedBytes<32>{
108
109 // Package the transaction data
110 let tx_hash_data = (target, value, func, data, timestamp);
111 // Encode the transaction data using ABI encoding
112 let tx_hash_bytes = TxIdHashType::abi_encode_sequence(&tx_hash_data);
113 // Initialize a new Keccak256 hasher
114 let mut hasher = Keccak256::new();
115 // Update the hasher with the encoded bytes
116 hasher.update(tx_hash_bytes);
117 // Finalize the hash computation
118 let result = hasher.finalize();
119 // Convert the hash result to a vector
120 let result_vec = result.to_vec();
121 // Create a FixedBytes<32> instance from the result vector
122 // This is used as the transaction ID
123 alloy_primitives::FixedBytes::<32>::from_slice(&result_vec)
124 }
125
126 // The `deposit` method is payable, so it can receive funds.
127 #[payable]
128 pub fn deposit(&self) {
129 }
130
131 // Function to queue a transaction for execution
132 pub fn queue(
133 &mut self,
134 target: Address, // Target address for the transaction
135 value: U256, // Value to be transferred
136 func: String, // Function name to be called
137 data: Bytes, // Data to be passed to the function
138 timestamp: U256, // Timestamp for the transaction
139 ) -> Result<(), TimeLockError> {
140 // Check if the caller is the owner of the contract
141 if self.owner.get() != msg::sender() {
142 // If not, return an error indicating the caller is not the owner
143 return Err(TimeLockError::NotOwnerError(NotOwnerError{}));
144 };
145
146 // Calculate a transaction ID using the provided parameters
147 let tx_id = self.get_tx_id(target, value, func.clone(), data.clone(), timestamp);
148 // Check if the transaction is already queued
149 if self.queued.get(tx_id) {
150 return Err(TimeLockError::AlreadyQueuedError(AlreadyQueuedError{txId: tx_id.into()}));
151 }
152
153 // Check if the provided timestamp is within the allowed range
154 if timestamp < U256::from(block::timestamp()) + U256::from(TimeLock::MIN_DELAY)
155 || timestamp > U256::from(block::timestamp()) + U256::from(TimeLock::MAX_DELAY)
156 {
157 return Err(TimeLockError::TimestampNotInRangeError(TimestampNotInRangeError{blockTimestamp: U256::from(block::timestamp()),timestamp: timestamp}));
158 }
159
160 // Set the transaction as queued in the contract's state
161 let mut queue_id = self.queued.setter(tx_id);
162 queue_id.set(true);
163 // Log the Queue event
164 evm::log(Queue {
165 txId: tx_id.into(),
166 target,
167 value: value,
168 func: func,
169 data: data.to_vec().into(),
170 timestamp: timestamp,
171 });
172 // If all checks pass and the transaction is successfully queued, return Ok
173 Ok(())
174 }
175
176 // Function to execute a queued transaction
177 pub fn execute(
178 &mut self,
179 target: Address, // Target address for the transaction
180 value: U256, // Value to be transferred
181 func: String, // Function name to be called
182 data: Bytes, // Data to be passed to the function
183 timestamp: U256, // Timestamp for the transaction
184 ) -> Result<(), TimeLockError> {
185 // Check if the caller is the owner of the contract
186 if self.owner.get() != msg::sender() {
187 // If not, return an error indicating the caller is not the owner
188 return Err(TimeLockError::NotOwnerError(NotOwnerError{}));
189 };
190
191 // Calculate a transaction ID using the provided parameters
192 let tx_id = self.get_tx_id(target, value, func.clone(), data.clone(), timestamp);
193 // Check if the transaction is not queued
194 if !self.queued.get(tx_id) {
195 return Err(TimeLockError::NotQueuedError(NotQueuedError{txId: tx_id.into()}));
196 }
197
198 // ----|-------------------|-------
199 // timestamp timestamp + grace period
200
201 // Check if the timestamp has passed
202 if U256::from(block::timestamp()) < timestamp {
203 return Err(TimeLockError::TimestampNotPassedError(TimestampNotPassedError{blockTimestamp: U256::from(block::timestamp()), timestamp: timestamp}));
204 }
205
206 // Check if the timestamp has expired
207 if U256::from(block::timestamp()) > timestamp + U256::from(TimeLock::GRACE_PERIOD) {
208 return Err(TimeLockError::TimestampExpiredError(TimestampExpiredError{blockTimestamp: U256::from(block::timestamp()), expiresAt: timestamp + U256::from(TimeLock::GRACE_PERIOD)}));
209 }
210
211 // Set the transaction as not queued in the contract's state
212 let mut queue_id = self.queued.setter(tx_id);
213 queue_id.set(false);
214
215 // Clone the data variable to ensure its lifetime is long enough
216 // let cloned_data: Vec<u8> = data.clone().into();
217
218 // Prepare calldata
219 let mut hasher = Keccak256::new();
220 hasher.update(func.as_bytes());
221 // Get function selector
222 let hashed_function_selector = hasher.finalize();
223 // Combine function selector and input data
224 let calldata = [&hashed_function_selector[..4], &data].concat();
225
226 // Call the target contract with the provided parameters
227 match call(Call::new_in(self).value(value), target, &calldata) {
228 // Log the transaction execution if successful
229 Ok(_) => {
230 evm::log(Execute {
231 txId: tx_id.into(),
232 target,
233 value: value,
234 func: func,
235 data: data.to_vec().into(),
236 timestamp: timestamp,
237 });
238 Ok(())
239 },
240 // Return an error if the transaction fails
241 Err(_) => Err(TimeLockError::TxFailedError(TxFailedError{})),
242 }
243 }
244
245 // Function to cancel a queued transaction
246 pub fn cancel(
247 &mut self,
248 target: Address,
249 value: U256,
250 func: String,
251 data: Bytes,
252 timestamp: U256,
253 ) -> Result<(), TimeLockError> {
254 // Check if the caller is the owner of the contract
255 if self.owner.get() != msg::sender() {
256 // If not, return an error indicating the caller is not the owner
257 return Err(TimeLockError::NotOwnerError(NotOwnerError{}));
258 };
259
260 // Calculate a transaction ID using the provided parameters
261 let tx_id = self.get_tx_id(target, value, func, data, timestamp);
262 // Check if the transaction is not queued
263 if !self.queued.get(tx_id) {
264 return Err(TimeLockError::NotQueuedError(NotQueuedError{txId: tx_id.into()}));
265 }
266
267 // Set the transaction as not queued in the contract's state
268 let mut queue_id = self.queued.setter(tx_id);
269 queue_id.set(false);
270
271 // Log the transaction cancellation
272 evm::log(Cancel {
273 txId: tx_id.into(),
274 });
275
276 // Return Ok if the transaction is successfully cancelled
277 Ok(())
278 }
279
280
281}
1// Allow `cargo stylus export-abi` to generate a main function if the "export-abi" feature is enabled.
2#![cfg_attr(not(feature = "export-abi"), no_main)]
3extern crate alloc;
4
5use sha3::{Digest, Keccak256};
6use alloc::string::String;
7use alloy_primitives::{Address, FixedBytes, U256};
8use alloy_sol_types::{sol, sol_data::{Address as SOLAddress, Bytes as SOLBytes, *}, SolType};
9// Import items from the SDK. The prelude contains common traits and macros.
10use stylus_sdk::{abi::Bytes, block, call::{call, Call}, evm, msg, prelude::*};
11
12// Define the types of the contract's storage.
13type TxIdHashType = (SOLAddress, Uint<256>, SOLBytes, SOLBytes, Uint<256>);
14
15sol!{
16 error AlreadyInitialized();
17 error NotOwnerError();
18 error AlreadyQueuedError(bytes32 txId);
19 error TimestampNotInRangeError(uint256 blockTimestamp, uint256 timestamp);
20 error NotQueuedError(bytes32 txId);
21 error TimestampNotPassedError(uint256 blockTimestamp, uint256 timestamp);
22 error TimestampExpiredError(uint256 blockTimestamp, uint256 expiresAt);
23 error TxFailedError();
24
25 event Queue(
26 bytes32 indexed txId,
27 address indexed target,
28 uint256 value,
29 string func,
30 bytes data,
31 uint256 timestamp
32 );
33 event Execute(
34 bytes32 indexed txId,
35 address indexed target,
36 uint256 value,
37 string func,
38 bytes data,
39 uint256 timestamp
40 );
41 event Cancel(bytes32 indexed txId);
42}
43
44// Define persistent storage using the Solidity ABI.
45// `TimeLock` will be the entrypoint for the contract.
46sol_storage! {
47 // Define the contract's storage.
48 #[entrypoint]
49 pub struct TimeLock {
50 address owner;
51 mapping(bytes32 => bool) queued;
52 }
53}
54
55// Error types for the TimeLock contract
56#[derive(SolidityError)]
57pub enum TimeLockError {
58 // Error for when the contract is already initialized.
59 AlreadyInitialized(AlreadyInitialized),
60 // Error for when the sender is not the owner
61 NotOwnerError(NotOwnerError),
62 // Error for when the transaction is already queued
63 AlreadyQueuedError(AlreadyQueuedError),
64 // Error for when the timestamp is not in the range
65 TimestampNotInRangeError(TimestampNotInRangeError),
66 // Error for when the transaction is not queued
67 NotQueuedError(NotQueuedError),
68 // Error for when the timestamp has not yet passed
69 TimestampNotPassedError(TimestampNotPassedError),
70 // Error for when the timestamp has expired
71 TimestampExpiredError(TimestampExpiredError),
72 // Error for when a transaction fails
73 TxFailedError(TxFailedError),
74}
75
76// Marks `TimeLock` as a contract with the specified external methods
77#[public]
78impl TimeLock {
79
80 // Minimum delay allowed for a transaction
81 pub const MIN_DELAY: u64 = 10;
82 // Maximum delay allowed for a transaction
83 pub const MAX_DELAY: u64 = 1000;
84 // Grace period after the maximum delay
85 pub const GRACE_PERIOD: u64 = 1000;
86
87 pub fn initialize(&mut self) -> Result<(), TimeLockError> {
88 if self.owner.get() != Address::default() {
89 return Err(TimeLockError::AlreadyInitialized(AlreadyInitialized{}))
90 }
91 self.owner.set(msg::sender());
92 Ok(())
93 }
94
95 pub fn owner(&self) -> Address {
96 self.owner.get()
97 }
98
99 // Function to generate a transaction ID
100 pub fn get_tx_id(
101 &self,
102 target: Address, // Target address for the transaction
103 value: U256, // Value to be transferred
104 func: String, // Function name to be called
105 data: Bytes, // Data to be passed to the function
106 timestamp: U256, // Timestamp for the transaction
107 ) -> FixedBytes<32>{
108
109 // Package the transaction data
110 let tx_hash_data = (target, value, func, data, timestamp);
111 // Encode the transaction data using ABI encoding
112 let tx_hash_bytes = TxIdHashType::abi_encode_sequence(&tx_hash_data);
113 // Initialize a new Keccak256 hasher
114 let mut hasher = Keccak256::new();
115 // Update the hasher with the encoded bytes
116 hasher.update(tx_hash_bytes);
117 // Finalize the hash computation
118 let result = hasher.finalize();
119 // Convert the hash result to a vector
120 let result_vec = result.to_vec();
121 // Create a FixedBytes<32> instance from the result vector
122 // This is used as the transaction ID
123 alloy_primitives::FixedBytes::<32>::from_slice(&result_vec)
124 }
125
126 // The `deposit` method is payable, so it can receive funds.
127 #[payable]
128 pub fn deposit(&self) {
129 }
130
131 // Function to queue a transaction for execution
132 pub fn queue(
133 &mut self,
134 target: Address, // Target address for the transaction
135 value: U256, // Value to be transferred
136 func: String, // Function name to be called
137 data: Bytes, // Data to be passed to the function
138 timestamp: U256, // Timestamp for the transaction
139 ) -> Result<(), TimeLockError> {
140 // Check if the caller is the owner of the contract
141 if self.owner.get() != msg::sender() {
142 // If not, return an error indicating the caller is not the owner
143 return Err(TimeLockError::NotOwnerError(NotOwnerError{}));
144 };
145
146 // Calculate a transaction ID using the provided parameters
147 let tx_id = self.get_tx_id(target, value, func.clone(), data.clone(), timestamp);
148 // Check if the transaction is already queued
149 if self.queued.get(tx_id) {
150 return Err(TimeLockError::AlreadyQueuedError(AlreadyQueuedError{txId: tx_id.into()}));
151 }
152
153 // Check if the provided timestamp is within the allowed range
154 if timestamp < U256::from(block::timestamp()) + U256::from(TimeLock::MIN_DELAY)
155 || timestamp > U256::from(block::timestamp()) + U256::from(TimeLock::MAX_DELAY)
156 {
157 return Err(TimeLockError::TimestampNotInRangeError(TimestampNotInRangeError{blockTimestamp: U256::from(block::timestamp()),timestamp: timestamp}));
158 }
159
160 // Set the transaction as queued in the contract's state
161 let mut queue_id = self.queued.setter(tx_id);
162 queue_id.set(true);
163 // Log the Queue event
164 evm::log(Queue {
165 txId: tx_id.into(),
166 target,
167 value: value,
168 func: func,
169 data: data.to_vec().into(),
170 timestamp: timestamp,
171 });
172 // If all checks pass and the transaction is successfully queued, return Ok
173 Ok(())
174 }
175
176 // Function to execute a queued transaction
177 pub fn execute(
178 &mut self,
179 target: Address, // Target address for the transaction
180 value: U256, // Value to be transferred
181 func: String, // Function name to be called
182 data: Bytes, // Data to be passed to the function
183 timestamp: U256, // Timestamp for the transaction
184 ) -> Result<(), TimeLockError> {
185 // Check if the caller is the owner of the contract
186 if self.owner.get() != msg::sender() {
187 // If not, return an error indicating the caller is not the owner
188 return Err(TimeLockError::NotOwnerError(NotOwnerError{}));
189 };
190
191 // Calculate a transaction ID using the provided parameters
192 let tx_id = self.get_tx_id(target, value, func.clone(), data.clone(), timestamp);
193 // Check if the transaction is not queued
194 if !self.queued.get(tx_id) {
195 return Err(TimeLockError::NotQueuedError(NotQueuedError{txId: tx_id.into()}));
196 }
197
198 // ----|-------------------|-------
199 // timestamp timestamp + grace period
200
201 // Check if the timestamp has passed
202 if U256::from(block::timestamp()) < timestamp {
203 return Err(TimeLockError::TimestampNotPassedError(TimestampNotPassedError{blockTimestamp: U256::from(block::timestamp()), timestamp: timestamp}));
204 }
205
206 // Check if the timestamp has expired
207 if U256::from(block::timestamp()) > timestamp + U256::from(TimeLock::GRACE_PERIOD) {
208 return Err(TimeLockError::TimestampExpiredError(TimestampExpiredError{blockTimestamp: U256::from(block::timestamp()), expiresAt: timestamp + U256::from(TimeLock::GRACE_PERIOD)}));
209 }
210
211 // Set the transaction as not queued in the contract's state
212 let mut queue_id = self.queued.setter(tx_id);
213 queue_id.set(false);
214
215 // Clone the data variable to ensure its lifetime is long enough
216 // let cloned_data: Vec<u8> = data.clone().into();
217
218 // Prepare calldata
219 let mut hasher = Keccak256::new();
220 hasher.update(func.as_bytes());
221 // Get function selector
222 let hashed_function_selector = hasher.finalize();
223 // Combine function selector and input data
224 let calldata = [&hashed_function_selector[..4], &data].concat();
225
226 // Call the target contract with the provided parameters
227 match call(Call::new_in(self).value(value), target, &calldata) {
228 // Log the transaction execution if successful
229 Ok(_) => {
230 evm::log(Execute {
231 txId: tx_id.into(),
232 target,
233 value: value,
234 func: func,
235 data: data.to_vec().into(),
236 timestamp: timestamp,
237 });
238 Ok(())
239 },
240 // Return an error if the transaction fails
241 Err(_) => Err(TimeLockError::TxFailedError(TxFailedError{})),
242 }
243 }
244
245 // Function to cancel a queued transaction
246 pub fn cancel(
247 &mut self,
248 target: Address,
249 value: U256,
250 func: String,
251 data: Bytes,
252 timestamp: U256,
253 ) -> Result<(), TimeLockError> {
254 // Check if the caller is the owner of the contract
255 if self.owner.get() != msg::sender() {
256 // If not, return an error indicating the caller is not the owner
257 return Err(TimeLockError::NotOwnerError(NotOwnerError{}));
258 };
259
260 // Calculate a transaction ID using the provided parameters
261 let tx_id = self.get_tx_id(target, value, func, data, timestamp);
262 // Check if the transaction is not queued
263 if !self.queued.get(tx_id) {
264 return Err(TimeLockError::NotQueuedError(NotQueuedError{txId: tx_id.into()}));
265 }
266
267 // Set the transaction as not queued in the contract's state
268 let mut queue_id = self.queued.setter(tx_id);
269 queue_id.set(false);
270
271 // Log the transaction cancellation
272 evm::log(Cancel {
273 txId: tx_id.into(),
274 });
275
276 // Return Ok if the transaction is successfully cancelled
277 Ok(())
278 }
279
280
281}
1[package]
2name = "stylus-timelock-example"
3version = "0.1.0"
4edition = "2021"
5
6[dependencies]
7alloy-primitives = "=0.7.6"
8alloy-sol-types = "=0.7.6"
9mini-alloc = "0.4.2"
10stylus-sdk = "0.6.0"
11hex = "0.4.3"
12sha3 = "0.10.8"
13
14[features]
15export-abi = ["stylus-sdk/export-abi"]
16debug = ["stylus-sdk/debug"]
17
18[lib]
19crate-type = ["lib", "cdylib"]
20
21[profile.release]
22codegen-units = 1
23strip = true
24lto = true
25panic = "abort"
26opt-level = "s"
1[package]
2name = "stylus-timelock-example"
3version = "0.1.0"
4edition = "2021"
5
6[dependencies]
7alloy-primitives = "=0.7.6"
8alloy-sol-types = "=0.7.6"
9mini-alloc = "0.4.2"
10stylus-sdk = "0.6.0"
11hex = "0.4.3"
12sha3 = "0.10.8"
13
14[features]
15export-abi = ["stylus-sdk/export-abi"]
16debug = ["stylus-sdk/debug"]
17
18[lib]
19crate-type = ["lib", "cdylib"]
20
21[profile.release]
22codegen-units = 1
23strip = true
24lto = true
25panic = "abort"
26opt-level = "s"