Skip to content

Commit

Permalink
add time locked transactions example
Browse files Browse the repository at this point in the history
  • Loading branch information
saimeunt committed May 29, 2024
1 parent 5fcef3e commit ed6c9fc
Show file tree
Hide file tree
Showing 12 changed files with 571 additions and 2 deletions.
3 changes: 2 additions & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
scarb 2.6.4
scarb 2.6.4
starknet-foundry 0.24.0
13 changes: 13 additions & 0 deletions Scarb.lock
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,11 @@ version = "0.1.0"
name = "simple_vault"
version = "0.1.0"

[[package]]
name = "snforge_std"
version = "0.24.0"
source = "git+https://github.com/foundry-rs/starknet-foundry.git?tag=v0.24.0#95e9fb09cb91b3c05295915179ee1b55bf923653"

[[package]]
name = "storage"
version = "0.1.0"
Expand All @@ -121,6 +126,14 @@ version = "0.1.0"
name = "testing_how_to"
version = "0.1.0"

[[package]]
name = "timelock"
version = "0.1.0"
dependencies = [
"openzeppelin",
"snforge_std",
]

[[package]]
name = "upgradeable_contract"
version = "0.1.0"
Expand Down
2 changes: 1 addition & 1 deletion Scarb.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ test = "$(git rev-parse --show-toplevel)/scripts/test_resolver.sh"

[workspace.dependencies]
starknet = ">=2.6.3"
# snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.11.0" }
openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag="v0.11.0" }

# [workspace.dev-dependencies]
snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.24.0" }
# openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag="v0.11.0" }

[workspace.package]
Expand Down
1 change: 1 addition & 0 deletions listings/applications/timelock/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
target
17 changes: 17 additions & 0 deletions listings/applications/timelock/Scarb.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "timelock"
version.workspace = true
edition = "2023_11"

[dependencies]
starknet.workspace = true
# Starknet Foundry:
snforge_std.workspace = true
# OpenZeppelin:
openzeppelin.workspace = true

[scripts]
test.workspace = true

[[target.starknet-contract]]
casm = true
44 changes: 44 additions & 0 deletions listings/applications/timelock/src/erc721.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#[starknet::contract]
pub mod ERC721 {
use starknet::ContractAddress;
use openzeppelin::introspection::src5::SRC5Component;
use openzeppelin::token::erc721::ERC721Component;

component!(path: SRC5Component, storage: src5, event: SRC5Event);
component!(path: ERC721Component, storage: erc721, event: ERC721Event);

// ERC20Mixin
#[abi(embed_v0)]
impl ERC721MixinImpl = ERC721Component::ERC721MixinImpl<ContractState>;
impl ERC721InternalImpl = ERC721Component::InternalImpl<ContractState>;

#[storage]
struct Storage {
#[substorage(v0)]
src5: SRC5Component::Storage,
#[substorage(v0)]
erc721: ERC721Component::Storage,
}

#[event]
#[derive(Drop, starknet::Event)]
enum Event {
#[flat]
SRC5Event: SRC5Component::Event,
#[flat]
ERC721Event: ERC721Component::Event,
}

#[constructor]
fn constructor(
ref self: ContractState,
name: ByteArray,
symbol: ByteArray,
base_uri: ByteArray,
recipient: ContractAddress,
token_id: u256
) {
self.erc721.initializer(name, symbol, base_uri);
self.erc721._mint(recipient, token_id);
}
}
8 changes: 8 additions & 0 deletions listings/applications/timelock/src/lib.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#[cfg(test)]
mod tests {
#[feature("safe_dispatcher")]
mod test_timelock;
mod utils;
}
mod erc721;
mod timelock;
227 changes: 227 additions & 0 deletions listings/applications/timelock/src/tests/test_timelock.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
use core::panic_with_felt252;
use starknet::get_block_timestamp;
use core::poseidon::{PoseidonTrait, poseidon_hash_span};
use core::hash::{HashStateTrait, HashStateExTrait};
use snforge_std::{
cheat_caller_address, cheat_block_timestamp, CheatSpan, spy_events, SpyOn, EventSpy,
EventAssertions
};
use openzeppelin::access::ownable::OwnableComponent;
use openzeppelin::token::erc721::interface::IERC721DispatcherTrait;
use openzeppelin::token::erc721::erc721::ERC721Component;
use timelock::timelock::{TimeLock, ITimeLockDispatcherTrait, ITimeLockSafeDispatcherTrait};
use timelock::tests::utils::{TimeLockTestTrait, TOKEN_ID, OTHER};

#[test]
fn test_get_tx_id_success() {
let timelock_test = TimeLockTestTrait::setup();
let (target, selector, calldata, timestamp) = timelock_test.get_tx();
let tx_id = timelock_test.timelock.get_tx_id(target, selector, calldata, timestamp);
let hash = PoseidonTrait::new()
.update(target.into())
.update(selector.into())
.update(poseidon_hash_span(calldata))
.update(timestamp.into())
.finalize();
assert_eq!(tx_id, hash);
}

#[test]
fn test_queue_only_owner() {
let timelock_test = TimeLockTestTrait::setup();
let (target, selector, calldata, timestamp) = timelock_test.get_tx();
cheat_caller_address(timelock_test.timelock_address, OTHER(), CheatSpan::TargetCalls(1));
match timelock_test.timelock_safe.queue(target, selector, calldata, timestamp) {
Result::Ok(_) => panic_with_felt252('FAIL'),
Result::Err(panic_data) => {
assert_eq!(*panic_data.at(0), OwnableComponent::Errors::NOT_OWNER);
}
}
}

#[test]
fn test_queue_already_queued() {
let timelock_test = TimeLockTestTrait::setup();
let (target, selector, calldata, timestamp) = timelock_test.get_tx();
timelock_test.timelock.queue(target, selector, calldata, timestamp);
match timelock_test.timelock_safe.queue(target, selector, calldata, timestamp) {
Result::Ok(_) => panic_with_felt252('FAIL'),
Result::Err(panic_data) => {
assert_eq!(*panic_data.at(0), TimeLock::Errors::ALREADY_QUEUED);
}
}
}

#[test]
fn test_queue_timestamp_not_in_range() {
let timelock_test = TimeLockTestTrait::setup();
let (target, selector, calldata, timestamp) = timelock_test.get_tx();
match timelock_test.timelock_safe.queue(target, selector, calldata, 0) {
Result::Ok(_) => panic_with_felt252('FAIL'),
Result::Err(panic_data) => {
assert_eq!(*panic_data.at(0), TimeLock::Errors::TIMESTAMP_NOT_IN_RANGE);
}
}
match timelock_test
.timelock_safe
.queue(target, selector, calldata, timestamp + TimeLock::MAX_DELAY) {
Result::Ok(_) => panic_with_felt252('FAIL'),
Result::Err(panic_data) => {
assert_eq!(*panic_data.at(0), TimeLock::Errors::TIMESTAMP_NOT_IN_RANGE);
}
}
}

#[test]
fn test_queue_success() {
let timelock_test = TimeLockTestTrait::setup();
let (target, selector, calldata, timestamp) = timelock_test.get_tx();
let mut spy = spy_events(SpyOn::One(timelock_test.timelock_address));
let tx_id = timelock_test.timelock.queue(target, selector, calldata, timestamp);
spy
.assert_emitted(
@array![
(
timelock_test.timelock_address,
TimeLock::Event::Queue(
TimeLock::Queue { tx_id, target, selector, calldata, timestamp }
)
)
]
);
assert_eq!(tx_id, timelock_test.timelock.get_tx_id(target, selector, calldata, timestamp));
}

#[test]
fn test_execute_only_owner() {
let timelock_test = TimeLockTestTrait::setup();
let (target, selector, calldata, timestamp) = timelock_test.get_tx();
cheat_caller_address(timelock_test.timelock_address, OTHER(), CheatSpan::TargetCalls(1));
match timelock_test.timelock_safe.execute(target, selector, calldata, timestamp) {
Result::Ok(_) => panic_with_felt252('FAIL'),
Result::Err(panic_data) => {
assert_eq!(*panic_data.at(0), OwnableComponent::Errors::NOT_OWNER);
}
}
}

#[test]
fn test_execute_not_queued() {
let timelock_test = TimeLockTestTrait::setup();
let (target, selector, calldata, timestamp) = timelock_test.get_tx();
match timelock_test.timelock_safe.execute(target, selector, calldata, timestamp) {
Result::Ok(_) => panic_with_felt252('FAIL'),
Result::Err(panic_data) => { assert_eq!(*panic_data.at(0), TimeLock::Errors::NOT_QUEUED); }
}
}

#[test]
fn test_execute_timestamp_not_passed() {
let timelock_test = TimeLockTestTrait::setup();
let (target, selector, calldata, timestamp) = timelock_test.get_tx();
timelock_test.timelock.queue(target, selector, calldata, timestamp);
match timelock_test.timelock_safe.execute(target, selector, calldata, timestamp) {
Result::Ok(_) => panic_with_felt252('FAIL'),
Result::Err(panic_data) => {
assert_eq!(*panic_data.at(0), TimeLock::Errors::TIMESTAMP_NOT_PASSED);
}
}
}

#[test]
fn test_execute_timestamp_expired() {
let timelock_test = TimeLockTestTrait::setup();
let (target, selector, calldata, timestamp) = timelock_test.get_tx();
timelock_test.timelock.queue(target, selector, calldata, timestamp);
cheat_block_timestamp(
timelock_test.timelock_address,
timestamp + TimeLock::GRACE_PERIOD + 1,
CheatSpan::TargetCalls(1)
);
match timelock_test.timelock_safe.execute(target, selector, calldata, timestamp) {
Result::Ok(_) => panic_with_felt252('FAIL'),
Result::Err(panic_data) => {
assert_eq!(*panic_data.at(0), TimeLock::Errors::TIMESTAMP_EXPIRED);
}
}
}

#[test]
fn test_execute_success() {
let timelock_test = TimeLockTestTrait::setup();
let (target, selector, calldata, timestamp) = timelock_test.get_tx();
let tx_id = timelock_test.timelock.get_tx_id(target, selector, calldata, timestamp);
timelock_test.timelock.queue(target, selector, calldata, timestamp);
timelock_test.erc721.approve(timelock_test.timelock_address, TOKEN_ID);
cheat_block_timestamp(timelock_test.timelock_address, timestamp + 1, CheatSpan::TargetCalls(1));
let mut spy = spy_events(SpyOn::One(timelock_test.timelock_address));
timelock_test.timelock.execute(target, selector, calldata, timestamp);
spy
.assert_emitted(
@array![
(
timelock_test.timelock_address,
TimeLock::Event::Execute(
TimeLock::Execute { tx_id, target, selector, calldata, timestamp }
)
)
]
);
}

#[test]
fn test_execute_failed() {
let timelock_test = TimeLockTestTrait::setup();
let (target, selector, calldata, timestamp) = timelock_test.get_tx();
timelock_test.timelock.queue(target, selector, calldata, timestamp);
cheat_block_timestamp(timelock_test.timelock_address, timestamp + 1, CheatSpan::TargetCalls(1));
match timelock_test.timelock_safe.execute(target, selector, calldata, timestamp) {
Result::Ok(_) => panic_with_felt252('FAIL'),
Result::Err(panic_data) => {
assert_eq!(*panic_data.at(0), ERC721Component::Errors::UNAUTHORIZED);
}
}
}

#[test]
fn test_cancel_only_owner() {
let timelock_test = TimeLockTestTrait::setup();
let (target, selector, calldata, timestamp) = timelock_test.get_tx();
let tx_id = timelock_test.timelock.get_tx_id(target, selector, calldata, timestamp);
cheat_caller_address(timelock_test.timelock_address, OTHER(), CheatSpan::TargetCalls(1));
match timelock_test.timelock_safe.cancel(tx_id) {
Result::Ok(_) => panic_with_felt252('FAIL'),
Result::Err(panic_data) => {
assert_eq!(*panic_data.at(0), OwnableComponent::Errors::NOT_OWNER);
}
}
}

#[test]
fn test_cancel_not_queued() {
let timelock_test = TimeLockTestTrait::setup();
let (target, selector, calldata, timestamp) = timelock_test.get_tx();
let tx_id = timelock_test.timelock.get_tx_id(target, selector, calldata, timestamp);
match timelock_test.timelock_safe.cancel(tx_id) {
Result::Ok(_) => panic_with_felt252('FAIL'),
Result::Err(panic_data) => { assert_eq!(*panic_data.at(0), TimeLock::Errors::NOT_QUEUED); }
}
}

#[test]
fn test_cancel_success() {
let timelock_test = TimeLockTestTrait::setup();
let (target, selector, calldata, timestamp) = timelock_test.get_tx();
let tx_id = timelock_test.timelock.queue(target, selector, calldata, timestamp);
let mut spy = spy_events(SpyOn::One(timelock_test.timelock_address));
timelock_test.timelock.cancel(tx_id);
spy
.assert_emitted(
@array![
(
timelock_test.timelock_address,
TimeLock::Event::Cancel(TimeLock::Cancel { tx_id })
)
]
);
}
Loading

0 comments on commit ed6c9fc

Please sign in to comment.