Skip to main content

Use Case: Approval Magic Demo

Overview

This article focuses on the Approval Magic Demo, where a reactive contract listens for token approvals and automates token transfers using pre-approved assets. Traditional approval mechanisms often require manual steps, but this demo uses reactive contracts to automate approvals and transfers across multiple networks.

Key smart contracts in the demo include:

ApprovalService

ApprovalService automates user subscriptions and token approvals. Key functions:

subscribe(): Allows users to subscribe to the service by paying the required fee. It ensures the correct fee is provided and that the user is not already subscribed. Upon successful subscription, it emits a Subscribe event and marks the user as subscribed.

function subscribe() external payable {
require(msg.value == subscription_fee, 'Incorrect fee');
require(!subscribers[msg.sender], 'Already subscribed');
emit Subscribe(msg.sender);
subscribers[msg.sender] = true;
}

unsubscribe(): Allows a subscribed user to opt out of the service. It checks if the user is currently subscribed, emits an Unsubscribe event, and updates the user's subscription status to false.

function unsubscribe() external {
require(subscribers[msg.sender], 'Not subscribed');
emit Unsubscribe(msg.sender);
subscribers[msg.sender] = false;
}

onApproval(): The onApproval() function handles token approval events. It is triggered when an approval occurs, validating the caller and handling the approval logic. It calculates the gas cost for executing the approval, adjusts it based on a gas coefficient, and ensures the transaction is settled by calling the settle() function on the target contract.

function onApproval(
address rvm_id,
IApprovalClient target,
address approver,
address approved_token,
uint256 amount
) external authorizedSenderOnly rvmIdOnly(rvm_id) {
uint256 gas_init = gasleft();
target.onApproval(approver, approved_token, amount);
uint256 adjusted_gas_price = tx.gasprice * gas_coefficient * (extra_gas + gas_init - gasleft());
adjusted_gas_price = adjusted_gas_price > 100 ? 100 : adjusted_gas_price;
target.settle(adjusted_gas_price);
}

Gas Management: Gas cost is dynamically calculated during the approval process to ensure the transaction remains cost-efficient. A fail-safe mechanism unsubscribes users if they can't cover the gas costs, protecting the service from losses.

uint256 adjusted_gas_price = tx.gasprice * gas_coefficient * (extra_gas + gas_init - gasleft());

ApprovalListener

ApprovalListener listens for subscription and approval events, providing the automation of approval processes across different chains. By using predefined topics, the contract captures relevant events and triggers corresponding actions, such as subscribing and unsubscribing users from the service.

react(): Listens for events and determines whether the event is a subscription, unsubscription, or approval, triggering the corresponding action.

function react(
uint256 /* chain_id */,
address _contract,
uint256 topic_0,
uint256 topic_1,
uint256 topic_2,
uint256 /* topic_3 */,
bytes calldata data,
uint256 /* block_number */,
uint256 /* op_code */
) external vmOnly {

Subscription Events: Handles subscription events by encoding the subscribe() function and emitting a callback to process the subscription.

if (topic_0 == SUBSCRIBE_TOPIC_0) {
bytes memory payload = abi.encodeWithSignature(
"subscribe(address,address)",
address(0),
address(uint160(topic_1))
);
emit Callback(REACTIVE_CHAIN_ID, address(this), CALLBACK_GAS_LIMIT, payload);
}

Unsubscription Events: Processes unsubscription events, encoding the unsubscribe() function and emitting a callback to remove the user from the service.

else if (topic_0 == UNSUBSCRIBE_TOPIC_0) {
bytes memory payload = abi.encodeWithSignature(
"unsubscribe(address,address)",
address(0),
address(uint160(topic_1))
);
emit Callback(REACTIVE_CHAIN_ID, address(this), CALLBACK_GAS_LIMIT, payload);
}

Approval Events: Decodes approval event data, constructs the onApproval payload, and emits a callback to handle token approval logic.

else {
(uint256 amount) = abi.decode(data, (uint256));
bytes memory payload = abi.encodeWithSignature(
"onApproval(address,address,address,address,uint256)",
address(0),
address(uint160(topic_2)),
address(uint160(topic_1)),
_contract,
amount
);
emit Callback(SEPOLIA_CHAIN_ID, address(approval_service), CALLBACK_GAS_LIMIT, payload);
}

ApprovalDemoToken

ApprovalDemoToken is a sample ERC-20 token used to demonstrate approval mechanics. Users can request tokens, and once approved by the service contract, they receive their allocated tokens automatically.

function request() external {
require(!recipients[msg.sender], 'Already received yours');
_mint(msg.sender, 1 ether);
}

Client Contracts

Client contracts such as ApprovalEthExch and ApprovalMagicSwap demonstrate real-world use cases, automating token transfers and swaps upon receiving approval.

ApprovalEthExch

A basic token exchange that transfers approved tokens from the user to the contract. The onApproval() function facilitates the transfer of approved tokens from the user to the contract while ensuring adequate checks are in place. It verifies that the approved token is supported, the approved amount matches the allowance, the user has sufficient tokens, and the contract has enough ETH for payout. Upon passing these checks, the function transfers the tokens from the approver to the contract and sends the corresponding ETH back to the approver.

function onApproval(
address approver,
address approved_token,
uint256 amount
) external onlyService {
require(approved_token == address(token), 'Token not supported');
require(amount == token.allowance(approver, address(this)), 'Approved amount mismatch');
require(amount <= token.balanceOf(approver), 'Insufficient tokens');
require(amount <= address(this).balance, 'Insufficient funds for payout');
token.transferFrom(approver, address(this), amount);
payable(approver).transfer(amount);
}

ApprovalMagicSwap

This contract uses Uniswap to swap tokens automatically based on approvals. The onApproval function enables the automatic swapping of tokens on Uniswap. It first verifies that the approved token is one of the supported tokens (token0 or token1), checks that the approved amount matches the allowance, and confirms that the approver has sufficient token balance. After transferring the tokens from the approver to the contract, it approves the router for the token transfer and constructs a path for the swap. Finally, it executes the token swap via Uniswap and transfers the resulting tokens back to the approver.

function onApproval(
address approver,
address approved_token,
uint256 amount
) external onlyService {
require(approved_token == address(token0) || approved_token == address(token1), 'Token not supported');
require(amount == IERC20(approved_token).allowance(approver, address(this)), 'Approved amount mismatch');
require(amount <= IERC20(approved_token).balanceOf(approver), 'Insufficient tokens');
assert(IERC20(approved_token).transferFrom(approver, address(this), amount));
assert(IERC20(approved_token).approve(address(router), amount));
address[] memory path = new address[](2);
path[0] = approved_token;
path[1] = approved_token == address(token0) ? address(token1) : address(token0);
uint256[] memory tokens = router.swapExactTokensForTokens(amount, 0, path, address(this), DEADLINE);
assert(IERC20(path[1]).transfer(approver, tokens[1]));
}

Further Considerations

Deploying these smart contracts in a live environment involves addressing key considerations:

  • Security: Ensuring security measures for token approvals and transfers to prevent unauthorized access.
  • Scalability: Managing a high volume of subscribers and transactions to maintain performance.
  • Gas Optimization: Reducing gas costs associated with approval handling to improve economic viability.
  • Interoperability: Expanding support to a wider range of tokens and networks to improve versatility.

Deployment & Testing

To deploy the contracts to Ethereum Sepolia, clone the project and follow these steps. Replace the relevant keys, addresses, and endpoints as needed. Make sure the following environment variables are correctly configured before proceeding:

IMPORTANT: The following assumes that ApprovalService and ApprovalListener are deployed using the same private key. ApprovalDemoToken and ApprovalEthExch can use other keys if needed.

Note: To receive REACT, send SepETH to the Reactive faucet on Ethereum Sepolia (0x9b9BB25f1A81078C544C829c5EB7822d747Cf434). An equivalent amount will be sent to your address.

Step 1 — Service Deployment

Current deployment addresses that can be reused:

export APPROVAL_SRV_ADDR=0xAaCc8a2D45a6427b9Dd1476f5D18599Fbb3B6Ac3
export APPROVAL_RCT_ADDR=0xd8f0861688c232bc874D983f0c8345cDB20146C6

The ApprovalService and ApprovalListener contracts can be deployed once and used by any number of clients.

ApprovalService Deployment

To deploy the ApprovalService contract, run the command given below. The constructor requires these arguments:

  • Subscription Fee (in Wei): 100
  • Gas Price Coefficient: 1
  • Extra Gas for Reactive Service: 10
forge create src/demos/approval-magic/ApprovalService.sol:ApprovalService --rpc-url $SEPOLIA_RPC --private-key $SEPOLIA_PRIVATE_KEY --constructor-args 100 1 10

The Deployed to address from the response should be assigned to APPROVAL_SRV_ADDR.

NOTE: To ensure a successful callback, APPROVAL_SRV_ADDR must have an ETH balance. Find more details here. To fund the contract, run the following command:

cast send $APPROVAL_SRV_ADDR --rpc-url $SEPOLIA_RPC --private-key $SEPOLIA_PRIVATE_KEY --value 0.1ether

To cover the debt of APPROVAL_SRV_ADDR, run this command:

cast send --rpc-url $SEPOLIA_RPC --private-key $SEPOLIA_PRIVATE_KEY $APPROVAL_SRV_ADDR "coverDebt()"

Alternatively, you can deposit funds into the Callback Proxy contract on Sepolia, using the command below. The EOA address whose private key signs the transaction pays the fee.

cast send --rpc-url $SEPOLIA_RPC --private-key $SEPOLIA_PRIVATE_KEY $SEPOLIA_CALLBACK_PROXY_ADDR "depositTo(address)" $APPROVAL_SRV_ADDR --value 0.1ether

Reactive Deployment

Deploy the ApprovalListener contract with the command shown below. Make sure to use the same private key (SEPOLIA_PRIVATE_KEY). Both contracts must be deployed from the same address as this ensures that the Sepolia contract can authenticate the RVM ID for callbacks.

forge create src/demos/approval-magic/ApprovalListener.sol:ApprovalListener --rpc-url $REACTIVE_RPC --private-key $SEPOLIA_PRIVATE_KEY --constructor-args $APPROVAL_SRV_ADDR

The Deployed to address should be assigned to APPROVAL_RCT_ADDR.

NOTE: To ensure a successful callback, APPROVAL_RCT_ADDR must have an ETH balance. Find more details here. To fund the contract, run the following command:

cast send $APPROVAL_RCT_ADDR --rpc-url $REACTIVE_RPC --private-key $REACTIVE_PRIVATE_KEY --value 0.1ether

To cover the debt of APPROVAL_RCT_ADDR, run this command:

cast send --rpc-url $REACTIVE_RPC --private-key $REACTIVE_PRIVATE_KEY $APPROVAL_RCT_ADDR "coverDebt()"

Alternatively, you can deposit funds into the Callback Proxy contract on Kopli Testnet, using the command below. The EOA address whose private key signs the transaction pays the fee.

cast send --rpc-url $REACTIVE_RPC --private-key $REACTIVE_PRIVATE_KEY $KOPLI_CALLBACK_PROXY_ADDR "depositTo(address)" $APPROVAL_RCT_ADDR --value 0.1ether

Step 2 — Demo Client Deployment

Token Deployment

Deploy the ApprovalDemoToken contract with the command given below. The constructor arguments are the name and symbol of the token you deploy. As an example, use the "FTW" value for both arguments.

forge create src/demos/approval-magic/ApprovalDemoToken.sol:ApprovalDemoToken --rpc-url $SEPOLIA_RPC --private-key $SEPOLIA_PRIVATE_KEY --constructor-args "FTW" "FTW"

The Deployed to address should be assigned to TOKEN_ADDR.

Client Deployment

Deploy the ApprovalEthExch contract with the following command:

forge create src/demos/approval-magic/ApprovalEthExch.sol:ApprovalEthExch --rpc-url $SEPOLIA_RPC --private-key $SEPOLIA_PRIVATE_KEY --constructor-args $APPROVAL_SRV_ADDR $TOKEN_ADDR

The Deployed to address should be assigned to EXCH_ADDR.

Step 3 — Fund and Subscribe

Fund the Exchanged Contract

Transfer 1000 tokens (Service Fee in Wei) to the exchange contract:

cast send $EXCH_ADDR --value 1000 --rpc-url $SEPOLIA_RPC --private-key $SEPOLIA_PRIVATE_KEY

Subscribe to Approval Service

Subscribe the exchange contract to the approval service:

cast send $EXCH_ADDR "subscribe()" --rpc-url $SEPOLIA_RPC --private-key $SEPOLIA_PRIVATE_KEY

NOTE: Dynamic subscription to approval events takes about 30 seconds, roughly twice Sepolia's block interval plus Reactive's block interval, before the service starts processing approvals for the new subscriber.

Step 4 — Test Approvals

Approve the transfer for 100 tokens (Tokens to exchange in Wei) and watch the magic happen:

cast send $TOKEN_ADDR "approve(address,uint256)" $EXCH_ADDR 100 --rpc-url $SEPOLIA_RPC --private-key $SEPOLIA_PRIVATE_KEY

Step 5 — Magic Swap Deployment

You can use two pre-deployed tokens or deploy your own (see the Token Deployment section).

export TOKEN1_ADDR=0xa5ac9324703CE29F3f859D449B78E5545E51763C
export TOKEN2_ADDR=0xf2988D2BDd377Bc91D0714F7c03014f381eA4a4e

You can request each token once as follows:

cast send $TOKEN1_ADDR "request()" --rpc-url $SEPOLIA_RPC --private-key $SEPOLIA_PRIVATE_KEY
cast send $TOKEN2_ADDR "request()" --rpc-url $SEPOLIA_RPC --private-key $SEPOLIA_PRIVATE_KEY

Token Deployment

Deploy two tokens with constructor arguments: "TOKEN_NAME" and "TOKEN_SYMBOL". As an example, use "TK1" for both arguments of the first token and "TK2" for the second.

forge create src/demos/approval-magic/ApprovalDemoToken.sol:ApprovalDemoToken --rpc-url $SEPOLIA_RPC --private-key $SEPOLIA_PRIVATE_KEY --constructor-args "TK1" "TK1"

The Deployed to address should be assigned to TOKEN1_ADDR.

forge create src/demos/approval-magic/ApprovalDemoToken.sol:ApprovalDemoToken --rpc-url $SEPOLIA_RPC --private-key $SEPOLIA_PRIVATE_KEY --constructor-args "TK2" "TK2"

The Deployed to address should be assigned to TOKEN2_ADDR.

Create Liquidity Pool

Create a liquidity pool for the two tokens using the Uniswap pair factory contract address 0x7E0987E5b3a30e3f2828572Bb659A548460a3003, which is a constant in this context.

cast send 0x7E0987E5b3a30e3f2828572Bb659A548460a3003 'createPair(address,address)' --rpc-url $SEPOLIA_RPC --private-key $SEPOLIA_PRIVATE_KEY $TOKEN1_ADDR $TOKEN2_ADDR

NOTE: Assign the Uniswap pair address from transaction logs on Sepolia scan to UNISWAP_PAIR_ADDR or export the pre-made pair for the tokens above:

export UNISWAP_PAIR_ADDR=0x3054DBa531fef1161774CAe65930CEAD2eE847bd

Add liquidity

Transfer tokens to the Uniswap pair:

cast send $TOKEN1_ADDR 'transfer(address,uint256)' --rpc-url $SEPOLIA_RPC --private-key $SEPOLIA_PRIVATE_KEY $UNISWAP_PAIR_ADDR 0.5ether
cast send $TOKEN2_ADDR 'transfer(address,uint256)' --rpc-url $SEPOLIA_RPC --private-key $SEPOLIA_PRIVATE_KEY $UNISWAP_PAIR_ADDR 0.5ether

Mint liquidity, using your EOA address (Client Wallet):

cast send $UNISWAP_PAIR_ADDR 'mint(address)' --rpc-url $SEPOLIA_RPC --private-key $SEPOLIA_PRIVATE_KEY $CLIENT_WALLET

Swap Deployment

Deploy the ApprovalMagicSwap contract:

forge create src/demos/approval-magic/ApprovalMagicSwap.sol:ApprovalMagicSwap --rpc-url $SEPOLIA_RPC --private-key $SEPOLIA_PRIVATE_KEY --constructor-args $APPROVAL_SRV_ADDR $TOKEN1_ADDR $TOKEN2_ADDR

The Deployed to address should be assigned to SWAP_ADDR.

Fund and Subscribe Swap Contract

If needed, export the pre-deployed magic swap contract:

export SWAP_ADDR=0x238860cAb697271612425A6E41EA2d7e6781E919

Transfer some funds to the swap contract and subscribe to the service:

cast send $SWAP_ADDR --value 100000 --rpc-url $SEPOLIA_RPC --private-key $SEPOLIA_PRIVATE_KEY
cast send $SWAP_ADDR "subscribe()" --rpc-url $SEPOLIA_RPC --private-key $SEPOLIA_PRIVATE_KEY

Step 6 — Test Swap

See the magic in action by approving one of the tokens (e.g., TOKEN1_ADDR) for the swap contract:

cast send $TOKEN1_ADDR "approve(address,uint256)" $SWAP_ADDR 0.1ether --rpc-url $SEPOLIA_RPC --private-key $SEPOLIA_PRIVATE_KEY