Skip to content

Commit

Permalink
Merge pull request #24 from multiversx/paymaster
Browse files Browse the repository at this point in the history
Paymaster SC
  • Loading branch information
sasurobert authored Oct 3, 2023
2 parents d23e2a8 + 64c591f commit fab80e1
Show file tree
Hide file tree
Showing 17 changed files with 792 additions and 0 deletions.
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ members = [
"contracts/order-book/factory/meta",
"contracts/order-book/pair",
"contracts/order-book/pair/meta",
"contracts/paymaster",
"contracts/paymaster/meta",
"contracts/ping-pong-egld",
"contracts/ping-pong-egld/meta",
"contracts/proxy-pause",
Expand Down
1 change: 1 addition & 0 deletions contracts/adder/src/adder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pub trait Adder {
}

/// Add desired amount to the storage variable.
#[payable("*")]
#[endpoint]
fn add(&self, value: BigUint) {
self.sum().update(|sum| *sum += value);
Expand Down
7 changes: 7 additions & 0 deletions contracts/paymaster/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Generated by Cargo
# will have compiled files and executables
/target/
*/target/

# The mxpy output
/output*/
23 changes: 23 additions & 0 deletions contracts/paymaster/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[package]
name = "paymaster"
version = "0.0.0"
authors = [ "MultiversX <[email protected]>" ]
edition = "2018"
publish = false
readme = "README.md"


[lib]
path = "src/paymaster.rs"

[dev-dependencies]
num-bigint = "0.4.2"

[dependencies.multiversx-sc]
version = "0.43.4"

[dev-dependencies.multiversx-sc-scenario]
version = "0.43.4"

[dev-dependencies.adder]
path = "../adder"
40 changes: 40 additions & 0 deletions contracts/paymaster/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Paymaster SC

## Overview

**Definition of problem**: the user needs to have a gas token (eGLD in case of MultiversX) to be able to do anything on the blockchain.
This is a hard concept to explain to non-crypto users.
Also gives some headaches for crypto users as well for the first steps of onboarding.
Imagine those people who bridge their NFTs or those who bridge USDC to a new account.
The new account can’t do anything until it does not have some eGLD to pay for the transactions.

In order for users to onboard faster to xPortal we introduced the concept of relayed transactions (we have v1, v2 and soon v3 of it).
This means a relayer can wrap the user transaction inside a relayedTX and in this case the relayer pays for the gas, but the user’s transaction is getting executed.
Right now, the relayers we use are free of charge, thus they do this service for new users for free, but they actually need eGLD to do the transactions.
When we speak about a few hundred users, this is not an issue, but when you want to scale up relaying transactions to thousands/millions of users, it becomes unsustainable.

## Implementation

Paymaster is a SC that makes relayed transactions sustainable.
This means that a user who doesn't own EGLD (native token) can make transactions by paying a fee in another token.

The Paymaster's objective is twofold:
- take a fee and send it to the Relayers;
- execute what the user wants to be executed.


The contract has only one endpoint: `forward_execution` and can be called by anyone.
The user will use `MultiESDTNFTTransfer` support to send multiple payments:
- first payment is always the fee that will be sent to the Relayer;
- rest of the payments will be what users want to send.

One example of userTX is:
```
MultiESDTNFTTransfer@paymasterSCAddr@feeTokenID@nonce@value@listofOther(tokenID,nonce,value)@forwardExecution@relayerAddr@destination@endpoint@extraArguments
```

After sending the Relayer the fee, `forward_execution` endpoint will make an *asynchronous call* to the destination.
The destionation can be a user or a smart contract.

We register a callback to the *asynchronous call*. In case of failure the paymaster SC sends the tokens back to the user.
14 changes: 14 additions & 0 deletions contracts/paymaster/meta/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "paymaster-meta"
version = "0.0.0"
edition = "2018"
publish = false
authors = [ "you",]

[dev-dependencies]

[dependencies.paymaster]
path = ".."

[dependencies.multiversx-sc-meta]
version = "0.43.4"
3 changes: 3 additions & 0 deletions contracts/paymaster/meta/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fn main() {
multiversx_sc_meta::cli_main::<paymaster::AbiProvider>();
}
3 changes: 3 additions & 0 deletions contracts/paymaster/multiversx.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"language": "rust"
}
18 changes: 18 additions & 0 deletions contracts/paymaster/mxsc-template.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name = "empty"
contract_trait = "EmptyContract"
src_file = "empty.rs"
rename_pairs = [
[
"blockchain.set_current_dir_from_workspace(\"contracts/examples/empty\");",
"// blockchain.set_current_dir_from_workspace(\"relative path to your workspace, if applicable\");",
],
]
files_include = [
"meta",
"scenarios",
"src",
"tests",
"wasm/Cargo.toml",
"Cargo.toml",
"multiversx.json",
]
39 changes: 39 additions & 0 deletions contracts/paymaster/scenarios/empty.scen.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "paymaster",
"steps": [
{
"step": "setState",
"accounts": {
"address:owner": {
"nonce": "1",
"balance": "0"
}
},
"newAddresses": [
{
"creatorAddress": "address:owner",
"creatorNonce": "1",
"newAddress": "sc:empty"
}
]
},
{
"step": "scDeploy",
"id": "deploy",
"tx": {
"from": "address:owner",
"contractCode": "file:../output/paymaster.wasm",
"arguments": [],
"gasLimit": "5,000,000",
"gasPrice": "0"
},
"expect": {
"out": [],
"status": "",
"logs": [],
"gas": "*",
"refund": "*"
}
}
]
}
53 changes: 53 additions & 0 deletions contracts/paymaster/src/forward_call.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
multiversx_sc::imports!();

pub type PaymentsVec<M> = ManagedVec<M, EsdtTokenPayment<M>>;

static ERR_CALLBACK_MSG: &[u8] = b"Error received in callback:";
pub const ESDT_TRANSFER_FUNC_NAME: &str = "ESDTTransfer";
#[multiversx_sc::module]
pub trait ForwardCall {
fn forward_call(
&self,
dest: ManagedAddress,
endpoint_name: ManagedBuffer,
endpoint_args: MultiValueEncoded<ManagedBuffer>,
payments: PaymentsVec<Self::Api>,
) {
let original_caller = self.blockchain().get_caller();

self.send()
.contract_call::<()>(dest, endpoint_name)
.with_raw_arguments(endpoint_args.to_arg_buffer())
.with_multi_token_transfer(payments)
.async_call()
.with_callback(self.callbacks().transfer_callback(original_caller))
.call_and_exit();
}

#[callback]
fn transfer_callback(
&self,
original_caller: ManagedAddress,
#[call_result] result: ManagedAsyncCallResult<MultiValueEncoded<ManagedBuffer>>,
) -> MultiValueEncoded<ManagedBuffer> {
let initial_payments = self.call_value().all_esdt_transfers();

match result {
ManagedAsyncCallResult::Ok(return_values) => return_values,
ManagedAsyncCallResult::Err(err) => {
if !initial_payments.is_empty() {
self.send()
.direct_multi(&original_caller, &initial_payments);
}

let mut err_result = MultiValueEncoded::new();
err_result.push(ManagedBuffer::new_from_bytes(ERR_CALLBACK_MSG));
err_result.push(err.err_msg.clone());

sc_print!("{}", err.err_msg);

err_result
}
}
}
}
39 changes: 39 additions & 0 deletions contracts/paymaster/src/paymaster.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#![no_std]

multiversx_sc::imports!();

pub mod forward_call;
const FEE_PAYMENT: usize = 0;

/// An empty contract. To be used as a template when starting a new contract from scratch.
#[multiversx_sc::contract]
pub trait PaymasterContract: forward_call::ForwardCall {
#[init]
fn init(&self) {}

#[endpoint(forwardExecution)]
#[payable("*")]
fn forward_execution(
&self,
relayer_addr: ManagedAddress,
dest: ManagedAddress,
endpoint_name: ManagedBuffer,
endpoint_args: MultiValueEncoded<ManagedBuffer>,
) {
let payments = self.call_value().all_esdt_transfers();
require!(!payments.is_empty(), "There is no fee for payment!");

let fee_payment = payments.get(FEE_PAYMENT);
self.send().direct_esdt(
&relayer_addr,
&fee_payment.token_identifier,
0,
&fee_payment.amount,
);

let mut payments_without_fee = payments.clone_value();
payments_without_fee.remove(FEE_PAYMENT);

self.forward_call(dest, endpoint_name, endpoint_args, payments_without_fee);
}
}
Loading

0 comments on commit fab80e1

Please sign in to comment.