Skip to content

Commit

Permalink
impl: liquidity share without proof::draft (#78)
Browse files Browse the repository at this point in the history
* impl: liquidity share without proof::draft

* implement `initiate_liquidity_withdrawal`

* implement liquidity withdrawal

* format

---------

Co-authored-by: 0xChqrles <[email protected]>
  • Loading branch information
ikemHood and 0xChqrles authored Sep 19, 2024
1 parent 404fc09 commit 9579a1f
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 13 deletions.
5 changes: 5 additions & 0 deletions contracts/Scarb.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,10 @@ casm = true
allowed-libfuncs-list.name = "audited"
build-external-contracts = ["openzeppelin_presets::erc20::ERC20Upgradeable"]

[profile.dev.cairo]
unstable-add-statements-functions-debug-info = true
unstable-add-statements-code-locations-debug-info = true
inlining-strategy= "avoid"

[scripts]
test = "snforge test"
2 changes: 1 addition & 1 deletion contracts/src/components/registry/interface.cairo
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use starknet::ContractAddress;
use zkramp::utils::hash::HashSerializable;

#[derive(Drop, Copy, Serde, Debug, PartialEq)]
#[derive(Drop, Copy, Serde, Debug, PartialEq, starknet::Store)]
pub enum OffchainId {
Revolut: felt252
}
Expand Down
19 changes: 17 additions & 2 deletions contracts/src/contracts/ramps/revolut/interface.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,40 @@ pub struct Proof {
foo: felt252
}

#[derive(Drop, Copy, Hash, Serde)]
#[derive(Drop, Copy, Hash, Serde, starknet::Store)]
pub struct LiquidityKey {
pub owner: ContractAddress,
pub offchain_id: OffchainId,
}

#[derive(Drop, Copy, starknet::Store)]
pub struct LiquidityShareRequest {
pub requestor: ContractAddress,
pub liquidity_key: LiquidityKey,
pub amount: u256,
pub expiration_date: u64,
}

#[starknet::interface]
pub trait IZKRampLiquidity<TState> {
fn add_liquidity(ref self: TState, amount: u256, offchain_id: OffchainId);
fn retrieve_liquidity(ref self: TState, liquidity_key: LiquidityKey);
fn initiate_liquidity_retrieval(ref self: TState, liquidity_key: LiquidityKey);
fn initiate_liquidity_withdrawal(
ref self: TState, liquidity_key: LiquidityKey, amount: u256, offchain_id: OffchainId
);
fn withdraw_liquidity(ref self: TState, liquidity_key: LiquidityKey, offchain_id: OffchainId, proof: Proof);
}

#[starknet::interface]
pub trait ZKRampABI<TState> {
// IZKRampLiquidity
fn add_liquidity(ref self: TState, amount: u256, offchain_id: OffchainId);
fn retrieve_liquidity(ref self: TState, liquidity_key: LiquidityKey);
fn initiate_liquidity_retrieval(ref self: TState, liquidity_key: LiquidityKey);
fn initiate_liquidity_withdrawal(
ref self: TState, liquidity_key: LiquidityKey, amount: u256, offchain_id: OffchainId
);
fn withdraw_liquidity(ref self: TState, liquidity_key: LiquidityKey, offchain_id: OffchainId, proof: Proof);

// IRegistry
fn is_registered(self: @TState, contract_address: ContractAddress, offchain_id: OffchainId) -> bool;
Expand Down
170 changes: 161 additions & 9 deletions contracts/src/contracts/ramps/revolut/revolut.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@ pub mod RevolutRamp {
use core::starknet::storage::{StoragePointerReadAccess};
use openzeppelin::access::ownable::OwnableComponent;
use starknet::storage::Map;
use starknet::{ContractAddress, get_caller_address};
use starknet::{ContractAddress, get_caller_address, get_block_timestamp};
use zkramp::components::escrow::escrow::EscrowComponent;
use zkramp::components::registry::interface::{OffchainId, IRegistry};
use zkramp::components::registry::registry::RegistryComponent;
use zkramp::contracts::ramps::revolut::interface::{LiquidityKey, IZKRampLiquidity};
use zkramp::contracts::ramps::revolut::interface::{LiquidityKey, IZKRampLiquidity, LiquidityShareRequest, Proof};

//
// Components
//

component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);
component!(path: RegistryComponent, storage: registry, event: RegistryEvent);
Expand All @@ -26,6 +30,13 @@ pub mod RevolutRamp {
// Escrow
impl EscrowImplImpl = EscrowComponent::EscrowImpl<ContractState>;

//
// Constants
//

const LOCK_DURATION_STEP: u64 = 900; // 15min
const MINIMUM_LOCK_DURATION: u64 = 3600; // 1h

//
// Storage
//
Expand All @@ -43,6 +54,10 @@ pub mod RevolutRamp {
liquidity: Map::<LiquidityKey, u256>,
// liquidity_key -> is_locked
locked_liquidity: Map::<LiquidityKey, bool>,
// (liquidity_key, timestamp) -> amount locked until timestamp is reached
locked_liquidity_shares: Map::<(LiquidityKey, u64), u256>,
// offchain_id -> liquidity share request
liquidity_share_request: Map::<OffchainId, LiquidityShareRequest>,
}

//
Expand All @@ -52,9 +67,14 @@ pub mod RevolutRamp {
pub mod Errors {
pub const NOT_REGISTERED: felt252 = 'Caller is not registered';
pub const INVALID_AMOUNT: felt252 = 'Invalid amount';
pub const WRONG_CALLER_ADDRESS: felt252 = 'Wrong caller address';
pub const EMPTY_LIQUIDITY_RETRIEVAL: felt252 = 'Empty liquidity retrieval';
pub const UNLOCKED_LIQUIDITY_RETRIEVAL: felt252 = 'Unlocked liquidity retrieval';
pub const CALLER_IS_NOT_OWNER: felt252 = 'Caller is not the owner';
pub const CALLER_IS_OWNER: felt252 = 'Caller is the owner';
pub const NULL_AMOUNT: felt252 = 'Amount cannot be null';
pub const UNLOCKED_LIQUIDITY: felt252 = 'Liquidity is unlocked';
pub const NOT_ENOUGH_LIQUDITY: felt252 = 'Not enough liquidity';
pub const LOCKED_LIQUIDITY_WITHDRAW: felt252 = 'Liquidity is not available';
pub const BUSY_OFFCHAIN_ID: felt252 = 'This offchainID is busy';
pub const LIQUIDITY_SHARE_NOT_AVAILABLE: felt252 = 'Liquidity share not available';
}

//
Expand All @@ -73,6 +93,8 @@ pub mod RevolutRamp {
LiquidityAdded: LiquidityAdded,
LiquidityLocked: LiquidityLocked,
LiquidityRetrieved: LiquidityRetrieved,
LiquidityShareRequested: LiquidityShareRequested,
LiquidityShareWithdrawn: LiquidityShareWithdrawn,
}

// Emitted when liquidity is added
Expand All @@ -98,6 +120,27 @@ pub mod RevolutRamp {
pub amount: u256,
}

// Emitted when a liquidity share is requested
#[derive(Drop, starknet::Event)]
pub struct LiquidityShareRequested {
#[key]
pub liquidity_key: LiquidityKey,
pub amount: u256,
pub requestor: ContractAddress,
pub offchain_id: OffchainId,
pub expiration_date: u64,
}

// Emitted when a liquidity share is withdrawn
#[derive(Drop, starknet::Event)]
pub struct LiquidityShareWithdrawn {
#[key]
pub liquidity_key: LiquidityKey,
pub amount: u256,
pub withdrawer: ContractAddress,
pub offchain_id: OffchainId,
}

//
// Constructor
//
Expand Down Expand Up @@ -142,13 +185,14 @@ pub mod RevolutRamp {
self.emit(LiquidityAdded { liquidity_key, amount });
}

/// Makes your liquidity unavailable, in order to retrieve it later.
fn initiate_liquidity_retrieval(ref self: ContractState, liquidity_key: LiquidityKey) {
let caller = get_caller_address();

// asserts liquidity amount is non null
assert(self.liquidity.read(liquidity_key).is_non_zero(), Errors::EMPTY_LIQUIDITY_RETRIEVAL);
assert(self.liquidity.read(liquidity_key).is_non_zero(), Errors::NULL_AMOUNT);
// asserts caller is the liquidity owner
assert(liquidity_key.owner == caller, Errors::WRONG_CALLER_ADDRESS);
assert(liquidity_key.owner == caller, Errors::CALLER_IS_NOT_OWNER);

// locks liquidity
self.locked_liquidity.write(liquidity_key, true);
Expand All @@ -157,13 +201,14 @@ pub mod RevolutRamp {
self.emit(LiquidityLocked { liquidity_key });
}

/// Retrieve liquidity if locked and owned by the caller.
fn retrieve_liquidity(ref self: ContractState, liquidity_key: LiquidityKey) {
let caller = get_caller_address();

// asserts caller is the liquidity owner
assert(self.locked_liquidity.read(liquidity_key), Errors::UNLOCKED_LIQUIDITY_RETRIEVAL);
assert(self.locked_liquidity.read(liquidity_key), Errors::UNLOCKED_LIQUIDITY);
// asserts caller is the liquidity owner
assert(liquidity_key.owner == caller, Errors::WRONG_CALLER_ADDRESS);
assert(liquidity_key.owner == caller, Errors::CALLER_IS_NOT_OWNER);

let token = self.token.read();
let amount = self.liquidity.read(liquidity_key);
Expand All @@ -174,5 +219,112 @@ pub mod RevolutRamp {
// emits Liquidityretrieved event
self.emit(LiquidityRetrieved { liquidity_key, amount });
}

fn initiate_liquidity_withdrawal(
ref self: ContractState, liquidity_key: LiquidityKey, amount: u256, offchain_id: OffchainId
) {
let caller = get_caller_address();
let current_timestamp = get_block_timestamp();

// assert caller is not the liquidity owner
assert(liquidity_key.owner != caller, Errors::CALLER_IS_OWNER);
// assert liquidity is unlocked
assert(!self.locked_liquidity.read(liquidity_key), Errors::LOCKED_LIQUIDITY_WITHDRAW);
// assert caller registered the offchain ID
assert(self.registry.is_registered(contract_address: caller, :offchain_id), Errors::NOT_REGISTERED);
// assert offchain_id is not busy with another withdrawal
assert(
self.liquidity_share_request.read(offchain_id).expiration_date <= current_timestamp,
Errors::BUSY_OFFCHAIN_ID
);

// get actually available liquidity
let available_liquidity_amount = self._get_available_liquidity(:liquidity_key);

// assert requested amount is valid
assert(amount <= available_liquidity_amount, Errors::NOT_ENOUGH_LIQUDITY);

// compute liquidity share locking period
let expiration_date = self._get_next_timestamp_key(current_timestamp + MINIMUM_LOCK_DURATION);

// lock liquidity share amount
let locked_amount = self.locked_liquidity_shares.read((liquidity_key, expiration_date));
self.locked_liquidity_shares.write((liquidity_key, expiration_date), locked_amount + amount);

// save share request
self
.liquidity_share_request
.write(
offchain_id, LiquidityShareRequest { requestor: caller, liquidity_key, amount, expiration_date }
);

// emit event
self
.emit(
LiquidityShareRequested { liquidity_key, amount, requestor: caller, offchain_id, expiration_date }
)
}

fn withdraw_liquidity(
ref self: ContractState, liquidity_key: LiquidityKey, offchain_id: OffchainId, proof: Proof
) {
let caller = get_caller_address();
let current_timestamp = get_block_timestamp();
let mut share_request = self.liquidity_share_request.read(offchain_id);
let token = self.token.read();

// assert caller has a valid pending withdrawal
assert(
share_request.expiration_date <= current_timestamp && share_request.requestor == caller,
Errors::LIQUIDITY_SHARE_NOT_AVAILABLE
);

// TODO: verify proof

// update liquidity amount
let amount = self.liquidity.read(liquidity_key);
self.liquidity.write(liquidity_key, amount - share_request.amount);

// update requested liquidity amount
let amount = self.locked_liquidity_shares.read((liquidity_key, share_request.expiration_date));
self
.locked_liquidity_shares
.write((liquidity_key, share_request.expiration_date), amount - share_request.amount);

// invalidate share request
share_request.expiration_date = 0;
self.liquidity_share_request.write(offchain_id, share_request);

// unlock funds
self.escrow.unlock(from: liquidity_key.owner, to: caller, :token, :amount);

// emit event
self.emit(LiquidityShareWithdrawn { liquidity_key, amount, withdrawer: caller, offchain_id })
}
}

//
// Internals
//

#[generate_trait]
impl InternalImpl of InternalTrait {
fn _get_available_liquidity(self: @ContractState, liquidity_key: LiquidityKey) -> u256 {
let mut amount = self.liquidity.read(liquidity_key);
let current_timestamp = get_block_timestamp();
let mut key_timestamp = self._get_next_timestamp_key(current_timestamp + MINIMUM_LOCK_DURATION);

while key_timestamp > current_timestamp {
amount -= self.locked_liquidity_shares.read((liquidity_key, key_timestamp));
key_timestamp -= LOCK_DURATION_STEP;
};

amount
}

fn _get_next_timestamp_key(self: @ContractState, after: u64) -> u64 {
// minus 1 in order to return `after` if it's already a valid key timestamp.
after - 1 + LOCK_DURATION_STEP - ((after - 1) % LOCK_DURATION_STEP)
}
}
}
2 changes: 1 addition & 1 deletion contracts/src/contracts/ramps/revolut/revolut_test.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ fn deploy_revolut_ramp() -> ZKRampABIDispatcher {
}

#[test]
#[should_panic(expected: 'Unlocked liquidity retrieval')]
#[should_panic(expected: 'Liquidity is unlocked')]
fn test_retrieve_uninitialized_liquidity_should_panic() {
let test_address: ContractAddress = test_address();

Expand Down

0 comments on commit 9579a1f

Please sign in to comment.