diff --git a/contracts/src/components/escrow/escrow.cairo b/contracts/src/components/escrow/escrow.cairo index a5a9b9c..8396af6 100644 --- a/contracts/src/components/escrow/escrow.cairo +++ b/contracts/src/components/escrow/escrow.cairo @@ -24,35 +24,6 @@ pub mod EscrowComponent { pub const INSUFFICIENT_BALANCE: felt252 = 'Insufficient deposit balance'; } - // - // EVENTS - // - #[event] - #[derive(Drop, starknet::Event)] - pub enum Event { - Locked: Locked, - UnLocked: UnLocked - } - - /// Emitted when the escrow is locked - #[derive(Drop, starknet::Event)] - pub struct Locked { - #[key] - pub token: ContractAddress, - pub from: ContractAddress, - pub amount: u256, - } - - /// Emitted when the escrow is unlocked - #[derive(Drop, starknet::Event)] - pub struct UnLocked { - #[key] - pub token: ContractAddress, - pub from: ContractAddress, - pub to: ContractAddress, - pub amount: u256, - } - // // Escrow impl // @@ -61,9 +32,7 @@ pub mod EscrowComponent { impl Escrow< TContractState, +HasComponent, +Drop, > of interface::IEscrow> { - fn lock_from( - ref self: ComponentState, from: ContractAddress, token: ContractAddress, amount: u256 - ) { + fn lock(ref self: ComponentState, from: ContractAddress, token: ContractAddress, amount: u256) { let locked_amount = self.deposits.read((from, token)); // transfers funds to escrow @@ -72,12 +41,9 @@ pub mod EscrowComponent { erc20_dispatcher.transfer_from(from, get_contract_address(), amount); self.deposits.write((from, token), amount + locked_amount); - - // emit event - self.emit(Locked { token, from, amount }); } - fn unlock_to( + fn unlock( ref self: ComponentState, from: ContractAddress, to: ContractAddress, @@ -100,8 +66,6 @@ pub mod EscrowComponent { // update locked amount self.deposits.write((from, token), locked_amount - amount); - // emit event - self.emit(UnLocked { token, from, to, amount }); } } } diff --git a/contracts/src/components/escrow/escrow_test.cairo b/contracts/src/components/escrow/escrow_test.cairo index 1071926..677a946 100644 --- a/contracts/src/components/escrow/escrow_test.cairo +++ b/contracts/src/components/escrow/escrow_test.cairo @@ -27,7 +27,7 @@ fn test_lock() { let mut escrow: ComponentState = Default::default(); - escrow.lock_from(constants::SPENDER(), token_dispatcher.contract_address, 42); + escrow.lock(constants::SPENDER(), token_dispatcher.contract_address, 42); assert_eq!(token_dispatcher.balance_of(constants::SPENDER()), 58); assert_eq!(token_dispatcher.allowance(constants::SPENDER(), constants::RECIPIENT()), 0); @@ -62,14 +62,14 @@ fn test_lock_unlock() { let mut escrow: ComponentState = Default::default(); - escrow.lock_from(constants::SPENDER(), token_dispatcher.contract_address, 42); + escrow.lock(constants::SPENDER(), token_dispatcher.contract_address, 42); start_cheat_caller_address(token_dispatcher.contract_address, get_contract_address()); token_dispatcher.approve(constants::RECIPIENT(), 42); start_cheat_caller_address(token_dispatcher.contract_address, constants::RECIPIENT()); - escrow.unlock_to(constants::SPENDER(), constants::RECIPIENT(), token_dispatcher.contract_address, 42); + escrow.unlock(constants::SPENDER(), constants::RECIPIENT(), token_dispatcher.contract_address, 42); assert_eq!(token_dispatcher.balance_of(constants::SPENDER()), 58); assert_eq!(token_dispatcher.balance_of(constants::RECIPIENT()), 42); @@ -123,19 +123,19 @@ fn test_lock_unlock_greater_than_balance() { let mut escrow: ComponentState = Default::default(); - escrow.lock_from(constants::SPENDER(), token_dispatcher.contract_address, 42); + escrow.lock(constants::SPENDER(), token_dispatcher.contract_address, 42); start_cheat_caller_address(token_dispatcher.contract_address, get_contract_address()); token_dispatcher.approve(constants::RECIPIENT(), 42); start_cheat_caller_address(token_dispatcher.contract_address, constants::RECIPIENT()); - escrow.unlock_to(constants::SPENDER(), constants::RECIPIENT(), token_dispatcher.contract_address, 420); + escrow.unlock(constants::SPENDER(), constants::RECIPIENT(), token_dispatcher.contract_address, 420); } #[test] #[should_panic(expected: 'ERC20: insufficient allowance')] -fn test_lock_from_unallowed_caller() { +fn test_lock_unallowed_caller() { let token_dispatcher = utils::setup_erc20(recipient: constants::OWNER()); start_cheat_caller_address(token_dispatcher.contract_address, constants::OWNER()); @@ -149,5 +149,5 @@ fn test_lock_from_unallowed_caller() { let mut escrow: ComponentState = Default::default(); - escrow.lock_from(constants::SPENDER(), token_dispatcher.contract_address, 42); + escrow.lock(constants::SPENDER(), token_dispatcher.contract_address, 42); } diff --git a/contracts/src/components/escrow/interface.cairo b/contracts/src/components/escrow/interface.cairo index 5539145..a47408c 100644 --- a/contracts/src/components/escrow/interface.cairo +++ b/contracts/src/components/escrow/interface.cairo @@ -2,6 +2,6 @@ use starknet::ContractAddress; #[starknet::interface] pub trait IEscrow { - fn lock_from(ref self: TState, from: ContractAddress, token: ContractAddress, amount: u256); - fn unlock_to(ref self: TState, from: ContractAddress, to: ContractAddress, token: ContractAddress, amount: u256); + fn lock(ref self: TState, from: ContractAddress, token: ContractAddress, amount: u256); + fn unlock(ref self: TState, from: ContractAddress, to: ContractAddress, token: ContractAddress, amount: u256); } diff --git a/contracts/src/contracts/ramps/revolut.cairo b/contracts/src/contracts/ramps/revolut.cairo index b4b5bf2..c886dee 100644 --- a/contracts/src/contracts/ramps/revolut.cairo +++ b/contracts/src/contracts/ramps/revolut.cairo @@ -1,2 +1,5 @@ pub mod interface; pub mod revolut; + +#[cfg(test)] +pub mod revolut_test; diff --git a/contracts/src/contracts/ramps/revolut/interface.cairo b/contracts/src/contracts/ramps/revolut/interface.cairo index 9dfadb4..120335b 100644 --- a/contracts/src/contracts/ramps/revolut/interface.cairo +++ b/contracts/src/contracts/ramps/revolut/interface.cairo @@ -2,11 +2,11 @@ use starknet::ContractAddress; use zkramp::components::registry::interface::OffchainId; #[derive(Drop, Serde)] -struct Proof { +pub struct Proof { foo: felt252 } -#[derive(Drop, Copy, Hash)] +#[derive(Drop, Copy, Hash, Serde)] pub struct LiquidityKey { pub owner: ContractAddress, pub offchain_id: OffchainId, @@ -15,4 +15,6 @@ pub struct LiquidityKey { #[starknet::interface] pub trait 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); } diff --git a/contracts/src/contracts/ramps/revolut/revolut.cairo b/contracts/src/contracts/ramps/revolut/revolut.cairo index 4b489cb..cbcc69a 100644 --- a/contracts/src/contracts/ramps/revolut/revolut.cairo +++ b/contracts/src/contracts/ramps/revolut/revolut.cairo @@ -1,15 +1,18 @@ #[starknet::contract] pub mod RevolutRamp { use core::num::traits::Zero; + use core::starknet::storage::{StoragePointerReadAccess}; use openzeppelin::access::ownable::OwnableComponent; use starknet::storage::Map; use starknet::{ContractAddress, get_caller_address}; + 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}; component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); component!(path: RegistryComponent, storage: registry, event: RegistryEvent); + component!(path: EscrowComponent, storage: escrow, event: EscrowEvent); // Ownable #[abi(embed_v0)] @@ -20,6 +23,9 @@ pub mod RevolutRamp { #[abi(embed_v0)] impl RegistryImpl = RegistryComponent::RegistryImpl; + // Escrow + impl EscrowImplImpl = EscrowComponent::EscrowImpl; + // // Storage // @@ -30,9 +36,13 @@ pub mod RevolutRamp { ownable: OwnableComponent::Storage, #[substorage(v0)] registry: RegistryComponent::Storage, + #[substorage(v0)] + escrow: EscrowComponent::Storage, token: ContractAddress, // liquidity_key -> amount liquidity: Map::, + // liquidity_key -> is_locked + locked_liquidity: Map::, } // @@ -42,22 +52,15 @@ 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 LOCKED_LIQUIDITY_retrievAL: felt252 = 'Locked liquidity retrieval'; } // // Events // - // Emitted when liquidity is added - #[derive(Drop, starknet::Event)] - pub struct LiquidityAdded { - #[key] - pub owner: ContractAddress, - #[key] - pub offchain_id: OffchainId, - pub amount: u256, - } - #[event] #[derive(Drop, starknet::Event)] enum Event { @@ -65,7 +68,34 @@ pub mod RevolutRamp { OwnableEvent: OwnableComponent::Event, #[flat] RegistryEvent: RegistryComponent::Event, + #[flat] + EscrowEvent: EscrowComponent::Event, LiquidityAdded: LiquidityAdded, + LiquidityLocked: LiquidityLocked, + LiquidityRetrieved: LiquidityRetrieved, + } + + // Emitted when liquidity is added + #[derive(Drop, starknet::Event)] + pub struct LiquidityAdded { + #[key] + pub liquidity_key: LiquidityKey, + pub amount: u256, + } + + // Emitted when liquidity is locked + #[derive(Drop, starknet::Event)] + pub struct LiquidityLocked { + #[key] + pub liquidity_key: LiquidityKey, + } + + // Emitted when liquidity is retrieved + #[derive(Drop, starknet::Event)] + pub struct LiquidityRetrieved { + #[key] + pub liquidity_key: LiquidityKey, + pub amount: u256, } // @@ -89,6 +119,7 @@ pub mod RevolutRamp { /// just increase the locked amount. fn add_liquidity(ref self: ContractState, amount: u256, offchain_id: OffchainId) { let caller = get_caller_address(); + let token = self.token.read(); // assert caller registered the offchain ID assert(self.registry.is_registered(contract_address: caller, :offchain_id), Errors::NOT_REGISTERED); @@ -101,8 +132,42 @@ pub mod RevolutRamp { let existing_amount = self.liquidity.read(liquidity_key); self.liquidity.write(liquidity_key, existing_amount + amount); + // unlocks liquidity + self.locked_liquidity.write(liquidity_key, true); + + // use the escrow to lock the funds + self.escrow.lock(from: caller, :token, :amount); + // Emit LiquidityAdded event - self.emit(LiquidityAdded { owner: caller, offchain_id, amount }); + self.emit(LiquidityAdded { liquidity_key, amount }); + } + + 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); + // asserts caller is the liquidity owner + assert(liquidity_key.owner == caller, Errors::WRONG_CALLER_ADDRESS); + + // locks liquidity + self.locked_liquidity.write(liquidity_key, true); + + // emits LiquidityLocked event + self.emit(LiquidityLocked { liquidity_key }); + } + + fn retrieve_liquidity(ref self: ContractState, liquidity_key: LiquidityKey) { + let caller = get_caller_address(); + + let token = self.token.read(); + let amount = self.liquidity.read(liquidity_key); + + // use the escrow to unlock the funds + self.escrow.unlock(from: caller, to: caller, :token, :amount); + + // emits Liquidityretrieved event + self.emit(LiquidityRetrieved { liquidity_key, amount }); } } } diff --git a/contracts/src/contracts/ramps/revolut/revolut_test.cairo b/contracts/src/contracts/ramps/revolut/revolut_test.cairo new file mode 100644 index 0000000..50461ae --- /dev/null +++ b/contracts/src/contracts/ramps/revolut/revolut_test.cairo @@ -0,0 +1,41 @@ +use core::hash::{HashStateTrait, HashStateExTrait}; +use core::poseidon::PoseidonTrait; + +use core::starknet::{ContractAddress, contract_address_const, get_caller_address}; + +use snforge_std::{declare, ContractClassTrait, start_cheat_caller_address, test_address}; +use zkramp::contracts::ramps::revolut::interface::{IzkRampABIDispatcher, IzkRampABIDispatcherTrait}; +use zkramp::contracts::ramps::revolut::revolut::RevolutRamp::RevolutImpl; + +use zkramp::tests::constants; + +fn deploy_revolut_ramp() -> (IzkRampABIDispatcher, ContractAddress) { + let contract = declare("RevolutRamp").unwrap(); + let owner: ContractAddress = contract_address_const::<'owner'>(); + let escrow: ContractAddress = contract_address_const::<'escrow'>(); + + let mut constructor_calldata = array![owner.into(), escrow.into()]; + + let (contract_address, _) = contract.deploy(@constructor_calldata).unwrap(); + + let dispatcher = IzkRampABIDispatcher { contract_address }; + + (dispatcher, contract_address) +} + +#[test] +#[should_panic(expected: 'Empty liquidity retrieval')] +fn test_retrieve_uninitialized_liquidity_should_panic() { + let test_address: ContractAddress = test_address(); + + start_cheat_caller_address(test_address, constants::CALLER()); + + let (revolut_ramp, _) = deploy_revolut_ramp(); + + let liquidity_id = PoseidonTrait::new() + .update_with(get_caller_address()) + .update_with(constants::REVOLUT_ID()) + .finalize(); + + revolut_ramp.retrieve_liquidity(liquidity_id); +}