Skip to content

Commit

Permalink
Vesting Component (#1116)
Browse files Browse the repository at this point in the history
* Add ERC20 utils

* Add finance module and vesting submodule

* Add interface for Vesting Component

* Implement Vesting Component

* Implement VestingWallet preset

* Run linter

* Update VestingSchedule trait to expect contract state as input param

* Add mocks for vesting tests

* Add tests for Vesting component

* Add tests for Vesting preset

* Add in-code documentation

* Add entry to changelog

* Fix linter issues

* Fix tests

* Fix batch of review issues

* Lint files

* Move default TestData init to helper function

* Fix review issues

* Fix review issues

* Add test_release_after_ownership_transferred test case

* Change the order of the events

* Fix minor review issues

* Fix compilation issues

* Add readme and update Scarb.toml

* Add tests for failed transfer assertion

* Fix readme, remove vesting keyword, add missing trait import

* Fix typo

* Make final review fixes
  • Loading branch information
immrsd authored Sep 18, 2024
1 parent a53ae0d commit 5cea01f
Show file tree
Hide file tree
Showing 26 changed files with 1,395 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- `into_base_16_string_no_padding` function to the test helpers (#1137)
- Vesting component and VestingWallet preset (#334)

### Changed

Expand Down
14 changes: 14 additions & 0 deletions Scarb.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ version = "0.16.0"
dependencies = [
"openzeppelin_access",
"openzeppelin_account",
"openzeppelin_finance",
"openzeppelin_governance",
"openzeppelin_introspection",
"openzeppelin_merkle_tree",
Expand Down Expand Up @@ -42,6 +43,17 @@ dependencies = [
"snforge_std",
]

[[package]]
name = "openzeppelin_finance"
version = "0.16.0"
dependencies = [
"openzeppelin_access",
"openzeppelin_test_common",
"openzeppelin_testing",
"openzeppelin_token",
"snforge_std",
]

[[package]]
name = "openzeppelin_governance"
version = "0.16.0"
Expand Down Expand Up @@ -72,6 +84,7 @@ version = "0.16.0"
dependencies = [
"openzeppelin_access",
"openzeppelin_account",
"openzeppelin_finance",
"openzeppelin_introspection",
"openzeppelin_test_common",
"openzeppelin_testing",
Expand All @@ -94,6 +107,7 @@ version = "0.16.0"
dependencies = [
"openzeppelin_access",
"openzeppelin_account",
"openzeppelin_finance",
"openzeppelin_testing",
"openzeppelin_token",
"openzeppelin_upgrades",
Expand Down
2 changes: 2 additions & 0 deletions Scarb.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
members = [
"packages/access",
"packages/account",
"packages/finance",
"packages/governance",
"packages/introspection",
"packages/merkle_tree",
Expand Down Expand Up @@ -47,6 +48,7 @@ snforge_std = "0.30.0"
starknet.workspace = true
openzeppelin_access = { path = "packages/access" }
openzeppelin_account = { path = "packages/account" }
openzeppelin_finance = { path = "packages/finance" }
openzeppelin_governance = { path = "packages/governance" }
openzeppelin_introspection = { path = "packages/introspection" }
openzeppelin_merkle_tree = { path = "packages/merkle_tree" }
Expand Down
2 changes: 1 addition & 1 deletion packages/account/src/utils.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ pub fn execute_calls(mut calls: Array<Call>) -> Array<Span<felt252>> {
let _res = execute_single_call(call);
res.append(_res);
},
Option::None(_) => { break (); },
Option::None => { break (); },
};
};
res
Expand Down
11 changes: 11 additions & 0 deletions packages/finance/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
## Finance

This crate includes primitives for financial systems.

### Interfaces

- `IVesting`

### Components

- `VestingComponent`
39 changes: 39 additions & 0 deletions packages/finance/Scarb.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
[package]
name = "openzeppelin_finance"
readme = "README.md"
keywords = [
"openzeppelin",
"starknet",
"contracts",
"finance",
"vesting"
]
version.workspace = true
edition.workspace = true
cairo-version.workspace = true
scarb-version.workspace = true
authors.workspace = true
description.workspace = true
documentation.workspace = true
repository.workspace = true
license-file.workspace = true

[tool]
fmt.workspace = true

[dependencies]
starknet.workspace = true
openzeppelin_access = { path = "../access" }
openzeppelin_token = { path = "../token" }

[dev-dependencies]
snforge_std.workspace = true
openzeppelin_testing = { path = "../testing" }
openzeppelin_test_common = { path = "../test_common" }

[lib]

[[target.starknet-contract]]
allowed-libfuncs-list.name = "experimental"
sierra = true
casm = false
2 changes: 2 additions & 0 deletions packages/finance/src/lib.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
mod tests;
pub mod vesting;
7 changes: 7 additions & 0 deletions packages/finance/src/tests.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#[cfg(test)]
pub(crate) mod common;
pub(crate) mod mocks;
#[cfg(test)]
mod test_vesting_linear;
#[cfg(test)]
mod test_vesting_steps;
71 changes: 71 additions & 0 deletions packages/finance/src/tests/common.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
use openzeppelin_finance::vesting::interface::IVestingDispatcher;
use openzeppelin_testing as utils;
use openzeppelin_testing::constants;
use openzeppelin_token::erc20::interface::IERC20Dispatcher;
use openzeppelin_utils::serde::SerializedAppend;
use starknet::{ContractAddress, SyscallResultTrait};

#[derive(Copy, Drop)]
pub(crate) enum VestingStrategy {
Linear,
Steps: u64
}

#[derive(Copy, Drop)]
pub(crate) struct TestData {
pub strategy: VestingStrategy,
pub total_allocation: u256,
pub beneficiary: ContractAddress,
pub start: u64,
pub duration: u64,
pub cliff_duration: u64
}

fn deploy_vesting_mock(data: TestData) -> IVestingDispatcher {
let contract_address = match data.strategy {
VestingStrategy::Linear => {
let mut calldata = array![];
calldata.append_serde(data.beneficiary);
calldata.append_serde(data.start);
calldata.append_serde(data.duration);
calldata.append_serde(data.cliff_duration);
utils::declare_and_deploy("LinearVestingMock", calldata)
},
VestingStrategy::Steps(total_steps) => {
let mut calldata = array![];
calldata.append_serde(total_steps);
calldata.append_serde(data.beneficiary);
calldata.append_serde(data.start);
calldata.append_serde(data.duration);
calldata.append_serde(data.cliff_duration);
utils::declare_and_deploy("StepsVestingMock", calldata)
}
};
IVestingDispatcher { contract_address }
}

fn deploy_erc20_mock(recipient: ContractAddress, initial_supply: u256) -> IERC20Dispatcher {
let mut calldata = array![];
calldata.append_serde(constants::NAME());
calldata.append_serde(constants::SYMBOL());
calldata.append_serde(initial_supply);
calldata.append_serde(recipient);

let contract_address = utils::declare_and_deploy("ERC20Mock", calldata);
IERC20Dispatcher { contract_address }
}

pub(crate) fn setup(data: TestData) -> (IVestingDispatcher, ContractAddress) {
let vesting = deploy_vesting_mock(data);
let token = deploy_erc20_mock(vesting.contract_address, data.total_allocation);
(vesting, token.contract_address)
}

pub(crate) fn set_transfer_to_fail(token: ContractAddress, should_fail: bool) {
let mut calldata = array![];
calldata.append_serde(true);
starknet::syscalls::call_contract_syscall(
token, selector!("set_transfer_should_fail"), calldata.span()
)
.unwrap_syscall();
}
2 changes: 2 additions & 0 deletions packages/finance/src/tests/mocks.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub(crate) mod erc20_mocks;
pub(crate) mod vesting_mocks;
91 changes: 91 additions & 0 deletions packages/finance/src/tests/mocks/erc20_mocks.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#[starknet::contract]
pub(crate) mod ERC20Mock {
use openzeppelin_token::erc20::interface::IERC20;
use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl};
use starknet::ContractAddress;
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};

component!(path: ERC20Component, storage: erc20, event: ERC20Event);

#[abi(embed_v0)]
impl ERC20MetadataImpl = ERC20Component::ERC20MetadataImpl<ContractState>;

impl InternalImpl = ERC20Component::InternalImpl<ContractState>;

#[storage]
struct Storage {
transfer_should_fail: bool,
#[substorage(v0)]
erc20: ERC20Component::Storage
}

#[event]
#[derive(Drop, starknet::Event)]
enum Event {
#[flat]
ERC20Event: ERC20Component::Event
}

#[constructor]
fn constructor(
ref self: ContractState,
name: ByteArray,
symbol: ByteArray,
initial_supply: u256,
recipient: ContractAddress
) {
self.erc20.initializer(name, symbol);
self.erc20.mint(recipient, initial_supply);
}

#[abi(embed_v0)]
impl ERC20Impl of IERC20<ContractState> {
fn total_supply(self: @ContractState) -> u256 {
self.erc20.total_supply()
}

fn balance_of(self: @ContractState, account: ContractAddress) -> u256 {
self.erc20.balance_of(account)
}

fn allowance(
self: @ContractState, owner: ContractAddress, spender: ContractAddress
) -> u256 {
self.erc20.allowance(owner, spender)
}

fn transfer(ref self: ContractState, recipient: ContractAddress, amount: u256) -> bool {
if self.transfer_should_fail.read() {
false
} else {
self.erc20.transfer(recipient, amount)
}
}

fn transfer_from(
ref self: ContractState,
sender: ContractAddress,
recipient: ContractAddress,
amount: u256
) -> bool {
if self.transfer_should_fail.read() {
false
} else {
self.erc20.transfer_from(sender, recipient, amount)
}
}

fn approve(ref self: ContractState, spender: ContractAddress, amount: u256) -> bool {
self.erc20.approve(spender, amount)
}
}

#[generate_trait]
#[abi(per_item)]
impl ExternalImpl of ExternalTrait {
#[external(v0)]
fn set_transfer_should_fail(ref self: ContractState, should_fail: bool) {
self.transfer_should_fail.write(should_fail);
}
}
}
Loading

0 comments on commit 5cea01f

Please sign in to comment.