Skip to content

Commit

Permalink
feat: gas service (#3)
Browse files Browse the repository at this point in the history
* feat: add workspace for axelar gas service

* feat: add refund impl and set up tests

* feat: add impl for pay_native_gas_for_contract_call

* feat: implement collect_fees

chore: add back contract_id as output to the setup_emv test

chore: update tests to pass in correct token address into refund

chore: WIP - adding auth for refund methods and tests for emitted events

chore: unwrapping index value

* feat: update gas service unit tests

* chore: remove vec from and add emitted event to collect_fees api

* chore: cargo fmt

* Update contracts/axelar-gas-service/Cargo.toml

Co-authored-by: Milap Sheth <[email protected]>

* Update contracts/axelar-gas-service/Cargo.toml

Co-authored-by: Milap Sheth <[email protected]>

* Update contracts/axelar-gas-service/src/contract.rs

Co-authored-by: Milap Sheth <[email protected]>

* Update contracts/axelar-gas-service/src/test.rs

Co-authored-by: Milap Sheth <[email protected]>

* Update contracts/axelar-gas-service/src/error.rs

Co-authored-by: Milap Sheth <[email protected]>

* Update contracts/axelar-gas-service/src/event.rs

Co-authored-by: Milap Sheth <[email protected]>

* Update contracts/axelar-gas-service/src/event.rs

Co-authored-by: Milap Sheth <[email protected]>

* Update contracts/axelar-gas-service/src/event.rs

Co-authored-by: Milap Sheth <[email protected]>

* Update contracts/axelar-gas-service/src/event.rs

Co-authored-by: Milap Sheth <[email protected]>

* Update contracts/axelar-gas-service/src/interface.rs

Co-authored-by: Milap Sheth <[email protected]>

* Update contracts/axelar-gas-service/src/lib.rs

Co-authored-by: Milap Sheth <[email protected]>

* chore: addressing latest comments on gas-service PR

* chore: fmt code

chore: fmt code

chore: run cargo sort

* chore: reduce parameter list size in pay_gas_for_contract_call

* Update contracts/axelar-soroban-interfaces/src/axelar_gas_service.rs

Co-authored-by: Milap Sheth <[email protected]>

* Update contracts/axelar-soroban-interfaces/src/axelar_gas_service.rs

Co-authored-by: Milap Sheth <[email protected]>

* Update contracts/axelar-gas-service/src/contract.rs

Co-authored-by: Milap Sheth <[email protected]>

* Update contracts/axelar-soroban-interfaces/src/axelar_gas_service.rs

Co-authored-by: Milap Sheth <[email protected]>

* Update contracts/axelar-gas-service/src/contract.rs

Co-authored-by: Milap Sheth <[email protected]>

* Update packages/axelar-soroban-std/src/types.rs

Co-authored-by: Milap Sheth <[email protected]>

* Update contracts/axelar-soroban-interfaces/src/axelar_gas_service.rs

Co-authored-by: Milap Sheth <[email protected]>

* chore: refactor gas service

chore: incorporate add'l comments

* chore: update pay_gas_for_contract_call to use tranfer_from

* Update contracts/axelar-gas-service/src/event.rs

Co-authored-by: Milap Sheth <[email protected]>

* Update contracts/axelar-gas-service/src/contract.rs

Co-authored-by: Milap Sheth <[email protected]>

* Update contracts/axelar-gas-service/src/contract.rs

Co-authored-by: Milap Sheth <[email protected]>

* Update contracts/axelar-gas-service/src/test.rs

Co-authored-by: Milap Sheth <[email protected]>

* Update contracts/axelar-gas-service/src/test.rs

Co-authored-by: Milap Sheth <[email protected]>

* chore: fix tests with variable name change

---------

Co-authored-by: Milap Sheth <[email protected]>
Co-authored-by: Milap Sheth <[email protected]>
  • Loading branch information
3 people authored Mar 19, 2024
1 parent 65a0891 commit 64fdccf
Show file tree
Hide file tree
Showing 12 changed files with 376 additions and 1 deletion.
9 changes: 9 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ soroban-sdk = { version = "20.2.0" }
axelar-soroban-interfaces = { version = "^0.1.0", path = "contracts/axelar-soroban-interfaces" }
axelar-soroban-std = { version = "^0.1.0", path = "packages/axelar-soroban-std" }
axelar-auth-verifier = { version = "^0.1.0", path = "contracts/axelar-auth-verifier" }
axelar-gas-service = { version = "^0.1.0", path = "contracts/axelar-gas-service" }
axelar-gateway = { version = "^0.1.0", path = "contracts/axelar-gateway" }

[profile.release]
Expand Down
16 changes: 16 additions & 0 deletions contracts/axelar-gas-service/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "axelar-gas-service"
version = "0.1.0"
edition = { workspace = true }

[lib]
crate-type = ["cdylib"]

[dependencies]
axelar-soroban-interfaces = { workspace = true }
axelar-soroban-std = { workspace = true }
soroban-sdk = { workspace = true }

[dev_dependencies]
soroban-sdk = { workspace = true, features = ["testutils"] }
axelar-soroban-std = { workspace = true, features = ["testutils"] }
107 changes: 107 additions & 0 deletions contracts/axelar-gas-service/src/contract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
use soroban_sdk::{contract, contractimpl, panic_with_error, token, Address, Bytes, Env, String};

use axelar_soroban_std::types::Token;

use crate::storage_types::DataKey;
use crate::{error::Error, event};
use axelar_soroban_interfaces::axelar_gas_service::AxelarGasServiceInterface;

#[contract]
pub struct AxelarGasService;

#[contractimpl]
impl AxelarGasServiceInterface for AxelarGasService {
fn initialize(env: Env, gas_collector: Address) {
if env
.storage()
.instance()
.get(&DataKey::Initialized)
.unwrap_or(false)
{
panic!("Already initialized");
}

env.storage().instance().set(&DataKey::Initialized, &true);

env.storage()
.instance()
.set(&DataKey::GasCollector, &gas_collector);
}

fn pay_gas_for_contract_call(
env: Env,
sender: Address,
destination_chain: String,
destination_address: String,
payload: Bytes,
refund_address: Address,
token: Token,
) {
sender.require_auth();

if token.amount <= 0 {
panic_with_error!(env, Error::InvalidAmount);
}

token::Client::new(&env, &token.address).transfer_from(
&env.current_contract_address(),
&sender,
&env.current_contract_address(),
&token.amount,
);

event::gas_paid_for_contract_call(
&env,
sender,
destination_chain,
destination_address,
payload,
refund_address,
token,
);
}

fn collect_fees(env: Env, receiver: Address, token: Token) {
let gas_collector: Address = env
.storage()
.instance()
.get(&DataKey::GasCollector)
.unwrap();

gas_collector.require_auth();

if token.amount <= 0 {
panic_with_error!(env, Error::InvalidAmount);
}

let token_client = token::Client::new(&env, &token.address);

let contract_token_balance = token_client.balance(&env.current_contract_address());

if contract_token_balance >= token.amount {
token_client.transfer(&env.current_contract_address(), &receiver, &token.amount)
} else {
panic_with_error!(env, Error::InsufficientBalance);
}

event::fee_collected(&env, gas_collector, token);
}

fn refund(env: Env, message_id: String, receiver: Address, token: Token) {
let gas_collector: Address = env
.storage()
.instance()
.get(&DataKey::GasCollector)
.unwrap();

gas_collector.require_auth();

token::Client::new(&env, &token.address).transfer(
&env.current_contract_address(),
&receiver,
&token.amount,
);

event::refunded(&env, message_id, receiver, token);
}
}
10 changes: 10 additions & 0 deletions contracts/axelar-gas-service/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use soroban_sdk::contracterror;

#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[repr(u32)]
pub enum Error {
InvalidAddress = 1,
InvalidAmount = 2,
InsufficientBalance = 3,
}
33 changes: 33 additions & 0 deletions contracts/axelar-gas-service/src/event.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use axelar_soroban_std::types::Token;
use soroban_sdk::{symbol_short, Address, Bytes, Env, String};

pub(crate) fn gas_paid_for_contract_call(
env: &Env,
sender: Address,
destination_chain: String,
destination_address: String,
payload: Bytes,
refund_address: Address,
token: Token,
) {
let topics = (
symbol_short!("gas_paid"),
env.crypto().keccak256(&payload),
sender,
destination_chain,
);
env.events().publish(
topics,
(destination_address, payload, refund_address, token),
);
}

pub(crate) fn refunded(env: &Env, message_id: String, receiver: Address, token: Token) {
let topics = (symbol_short!("refunded"), message_id, receiver, token);
env.events().publish(topics, ());
}

pub(crate) fn fee_collected(env: &Env, receiver: Address, token: Token) {
let topics = (symbol_short!("collected"), receiver, token);
env.events().publish(topics, ());
}
11 changes: 11 additions & 0 deletions contracts/axelar-gas-service/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#![no_std]

pub mod contract;
mod error;
mod event;
mod storage_types;

#[cfg(test)]
mod test;

pub use contract::AxelarGasServiceClient;
8 changes: 8 additions & 0 deletions contracts/axelar-gas-service/src/storage_types.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
use soroban_sdk::contracttype;

#[contracttype]
#[derive(Clone, Debug)]
pub enum DataKey {
Initialized,
GasCollector,
}
146 changes: 146 additions & 0 deletions contracts/axelar-gas-service/src/test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
#![cfg(test)]
extern crate std;

use std::format;

use axelar_soroban_std::{assert_emitted_event, types::Token};

use crate::contract::{AxelarGasService, AxelarGasServiceClient};
use soroban_sdk::{
bytes, symbol_short,
testutils::Address as _,
token::{StellarAssetClient, TokenClient},
Address, Env, String,
};

fn setup_env<'a>() -> (Env, Address, Address, AxelarGasServiceClient<'a>) {
let env = Env::default();

env.mock_all_auths();

let contract_id = env.register_contract(None, AxelarGasService);

let client = AxelarGasServiceClient::new(&env, &contract_id);
let gas_collector: Address = Address::generate(&env);
client.initialize(&gas_collector);

(env, contract_id, gas_collector, client)
}

#[test]
fn pay_gas_for_contract_call() {
let (env, contract_id, _, client) = setup_env();

let token_address: Address = env.register_stellar_asset_contract(Address::generate(&env));
let sender: Address = Address::generate(&env);
let gas_amount: i128 = 1;
let token = Token {
address: token_address.clone(),
amount: gas_amount,
};
let refund_address: Address = Address::generate(&env);
let payload = bytes!(&env, 0x1234);
let destination_chain: String = String::from_str(&env, "ethereum");
let destination_address: String =
String::from_str(&env, "0x4EFE356BEDeCC817cb89B4E9b796dB8bC188DC59");

let token_client = TokenClient::new(&env, &token_address);
StellarAssetClient::new(&env, &token_address).mint(&sender, &gas_amount);

let expiration_ledger = &env.ledger().sequence() + 200;

// approve token spend before invoking `pay_gas_for_contract_call`
token_client.approve(&sender, &contract_id, &gas_amount, &expiration_ledger);

assert_eq!(token_client.allowance(&sender, &contract_id), gas_amount);

client.pay_gas_for_contract_call(
&sender,
&destination_chain,
&destination_address,
&payload,
&refund_address,
&token,
);

assert_eq!(0, token_client.balance(&sender));
assert_eq!(gas_amount, token_client.balance(&contract_id));
assert_eq!(token_client.allowance(&sender, &contract_id), 0);

assert_emitted_event(
&env,
-1,
&contract_id,
(
symbol_short!("gas_paid"),
env.crypto().keccak256(&payload),
sender,
destination_chain,
),
(destination_address, payload, refund_address, token),
);
}

#[test]
fn collect_fees() {
let (env, contract_id, gas_collector, client) = setup_env();
let token_address: Address = env.register_stellar_asset_contract(Address::generate(&env));
let token_client = TokenClient::new(&env, &token_address);
let supply: i128 = 1000;
let refund_amount = 1;
let token = Token {
address: token_address.clone(),
amount: refund_amount,
};
StellarAssetClient::new(&env, &token.address).mint(&contract_id, &supply);

client.collect_fees(&gas_collector, &token);

assert_eq!(refund_amount, token_client.balance(&gas_collector));
assert_eq!(supply - refund_amount, token_client.balance(&contract_id));

assert_emitted_event(
&env,
-1,
&contract_id,
(symbol_short!("collected"), gas_collector, token),
(),
);
}

#[test]
fn refund() {
let (env, contract_id, _, client) = setup_env();
let token_address: Address = env.register_stellar_asset_contract(Address::generate(&env));
let token_client = TokenClient::new(&env, &token_address);
let supply: i128 = 1000;
StellarAssetClient::new(&env, &token_address).mint(&contract_id, &supply);

let receiver: Address = Address::generate(&env);
let refund_amount: i128 = 1;
let token = Token {
address: token_address.clone(),
amount: refund_amount,
};

let message_id = String::from_str(
&env,
&format!(
"{}-{}",
"0xfded3f55dec47250a52a8c0bb7038e72fa6ffaae33562f77cd2b629ef7fd424d", 0
),
);

client.refund(&message_id, &receiver, &token);

assert_eq!(refund_amount, token_client.balance(&receiver));
assert_eq!(supply - refund_amount, token_client.balance(&contract_id));

assert_emitted_event(
&env,
-1,
&contract_id,
(symbol_short!("refunded"), message_id, receiver, token),
(),
);
}
26 changes: 26 additions & 0 deletions contracts/axelar-soroban-interfaces/src/axelar_gas_service.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
use axelar_soroban_std::types::Token;
use soroban_sdk::{contractclient, Address, Bytes, Env, String};

/// Interface for the Axelar Gas Service.
#[contractclient(name = "AxelarGasServiceClient")]
pub trait AxelarGasServiceInterface {
/// Initialize the gas service contract with a gas_collector address.
fn initialize(env: Env, gas_collector: Address);

/// Pay for gas using a token for a contract call on a destination chain. This function is called on the source chain before calling the gateway to execute a remote contract.
fn pay_gas_for_contract_call(
env: Env,
sender: Address,
destination_chain: String,
destination_address: String,
payload: Bytes,
refund_address: Address,
token: Token,
);

/// Allows the `gas_collector` to collect accumulated fees from the contract. Only callable by the gas_collector.
fn collect_fees(env: Env, receiver: Address, token: Token);

/// Refunds gas payment to the receiver in relation to a specific cross-chain transaction. Only callable by the gas_collector.
fn refund(env: Env, message_id: String, receiver: Address, token: Token);
}
1 change: 1 addition & 0 deletions contracts/axelar-soroban-interfaces/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@

pub mod axelar_auth_verifier;
pub mod axelar_executable;
pub mod axelar_gas_service;
pub mod axelar_gateway;
Loading

0 comments on commit 64fdccf

Please sign in to comment.