Skip to content

Commit

Permalink
feat: its lock/unlock_fee example (#193)
Browse files Browse the repository at this point in the history
  • Loading branch information
benjamin852 authored Jul 25, 2024
1 parent 6c03476 commit 075dad5
Show file tree
Hide file tree
Showing 9 changed files with 291 additions and 26 deletions.
9 changes: 5 additions & 4 deletions examples/evm/its-canonical-token/CanonicalToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,28 @@ pragma solidity ^0.8.0;
import { ERC20 } from '@openzeppelin/contracts/token/ERC20/ERC20.sol';

import { Minter } from '@axelar-network/interchain-token-service/contracts/utils/Minter.sol';

/**
* @title InterchainToken
* @notice This contract implements an interchain token which extends InterchainToken functionality.
* @dev This contract also inherits Minter and Implementation logic.
*/
contract CanonicalToken is ERC20, Minter {
uint8 internal immutable decimals_;
uint8 internal immutable _decimals;

uint256 internal constant UINT256_MAX = 2 ** 256 - 1;

/**
* @notice Constructs the InterchainToken contract.
* @dev Makes the implementation act as if it has been setup already to disallow calls to init() (even though that would not achieve anything really).
*/
constructor(string memory name_, string memory symbol_, uint8 decimalsValue) ERC20(name_, symbol_) {
decimals_ = decimalsValue;
constructor(string memory _name, string memory _symbol, uint8 _decimalsValue) ERC20(_name, _symbol) {
_decimals = _decimalsValue;
_addMinter(msg.sender);
}

function decimals() public view override returns (uint8) {
return decimals_;
return _decimals;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion examples/evm/its-canonical-token/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ This example deploys the token on Fantom and Avalanche, it registers the token a

```bash
npm run deploy evm/its-canonical-token local
npm run execute evm/its-canonical-token local "Fantom" "Avalanche"```
npm run execute evm/its-canonical-token local "Fantom" "Avalanche"

The output will be:

Expand Down
11 changes: 7 additions & 4 deletions examples/evm/its-canonical-token/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const CanonicalToken = rootRequire('./artifacts/examples/evm/its-canonical-token

async function deploy(chain, wallet) {
console.log(`Deploying CanonicalToken for ${chain.name}.`);
chain.canonicalToken = await deployContract(wallet, CanonicalToken, ['Custon Token', 'CT', 18]);
chain.canonicalToken = await deployContract(wallet, CanonicalToken, ['Canonical Token', 'CT', 18]);
chain.wallet = wallet;
console.log(`Deployed CanonicalToken for ${chain.name} at ${chain.canonicalToken.address}.`);
}
Expand All @@ -24,20 +24,23 @@ async function execute(chains, wallet, options) {

const fee = await calculateBridgeFee(source, destination);

const sourceIts = new Contract(source.interchainTokenService, IInterchainTokenService.abi, wallet.connect(source.provider));
const destinationIts = new Contract(
destination.interchainTokenService,
IInterchainTokenService.abi,
wallet.connect(destination.provider),
);

const sourceFactory = new Contract(source.interchainTokenFactory, IInterchainTokenFactory.abi, wallet.connect(source.provider));

console.log(`Registering canonical token ${source.canonicalToken.address} at ${source.name}`);
await (await sourceFactory.registerCanonicalInterchainToken(source.canonicalToken.address)).wait();

console.log(`Deploy remote canonical token from ${source.name} to ${destination.name}`);
console.log(destination.name, 'the destination');
await (
await sourceFactory.deployRemoteCanonicalInterchainToken('', source.canonicalToken.address, destination.name, fee, { value: fee })
await sourceFactory.deployRemoteCanonicalInterchainToken(source.name, source.canonicalToken.address, destination.name, fee, {
value: fee,
})
).wait();

const tokenId = await sourceFactory.canonicalInterchainTokenId(source.canonicalToken.address);
Expand All @@ -51,7 +54,7 @@ async function execute(chains, wallet, options) {
console.log(`Minting ${amount} canonical tokens to ${wallet.address} at ${source.name}`);
await (await source.canonicalToken.mint(wallet.address, amount)).wait();

console.log(`Approving ${amount} canonical tokens to the token manager at ${source.name}`);
console.log(`Approving ${amount} canonical tokens to ITS at ${source.name}`);
await (await source.canonicalToken.approve(source.interchainTokenService, amount)).wait();

await interchainTransfer(source, destination, wallet, tokenId, amount, fee);
Expand Down
132 changes: 132 additions & 0 deletions examples/evm/its-lock-unlock-fee/FeeToken.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import { InterchainTokenStandard } from '@axelar-network/interchain-token-service/contracts/interchain-token/InterchainTokenStandard.sol';
import { ERC20 } from '@openzeppelin/contracts/token/ERC20/ERC20.sol';

import { Minter } from '@axelar-network/interchain-token-service/contracts/utils/Minter.sol';

/**
* @title FeeToken
* @notice This contract implements an interchain token which extends InterchainToken functionality and extracts a fee for each transfer.
* @dev This contract also inherits Minter and Implementation logic.
*/
contract FeeToken is InterchainTokenStandard, ERC20, Minter {
uint8 internal immutable _decimals;
bytes32 internal tokenId;
address internal immutable _interchainTokenService;

uint256 internal constant UINT256_MAX = 2 ** 256 - 1;

uint256 public feePercent;

/**
* @notice Constructs the InterchainToken contract.
* @dev Makes the implementation act as if it has been setup already to disallow calls to init() (even though that would not achieve anything really).
*/
constructor(
string memory _name,
string memory _symbol,
uint8 _decimalsValue,
uint256 _feePercent,
address _interchainTokenServiceAddress
) ERC20(_name, _symbol) {
_decimals = _decimalsValue;
_interchainTokenService = _interchainTokenServiceAddress;

feePercent = _feePercent;

_addMinter(msg.sender);
}

function decimals() public view override returns (uint8) {
return _decimals;
}

/**
* @notice set new fee percent
* @param _feePercent new fee percent
*/
function setFeePercent(uint256 _feePercent) external onlyRole(uint8(Roles.MINTER)) {
require(_feePercent <= 100, 'Fee percent too high');
feePercent = _feePercent;
}

/**
* @notice Returns the interchain token service
* @return address The interchain token service contract
*/
function interchainTokenService() public view override returns (address) {
return _interchainTokenService;
}

/**
* @notice Returns the tokenId for this token.
* @return bytes32 The token manager contract.
*/
function interchainTokenId() public view override returns (bytes32) {
return tokenId;
}

/**
* @notice Function to mint new tokens.
* @dev Can only be called by the minter address.
* @param account The address that will receive the minted tokens.
* @param amount The amount of tokens to mint.
*/
function mint(address account, uint256 amount) external onlyRole(uint8(Roles.MINTER)) {
_mint(account, amount);
}

/**
* @notice Function to burn tokens.
* @dev Can only be called by the minter address.
* @param account The address that will have its tokens burnt.
* @param amount The amount of tokens to burn.
*/
function burn(address account, uint256 amount) external onlyRole(uint8(Roles.MINTER)) {
_burn(account, amount);
}

function addMinter(address minter) external {
_addMinter(minter);
}

/**
* @notice transfer token from sender to recipient. Sender must approve tx to be sent
* @param from sender of the token
* @param to recipient of the token
* @param amount amount of tokens to be sent
*/
function transferFrom(address from, address to, uint256 amount) public override returns (bool) {
uint256 fee = (amount * feePercent) / 1e18;
uint256 amountAfterFee = amount - fee;

super.transferFrom(from, to, amountAfterFee);

/**
* NOTE: Burning of the fee is just one of
* many options here. For simplicity the fee is getting
* burnt, but you can instead for example:
* 1. Send to reward pool
* 2. Send to treasury
* 3. Send to liquidity pool
*/
_burn(from, fee);

return true;
}

/**
* @notice A method to be overwritten that will decrease the allowance of the `spender` from `sender` by `amount`.
* @dev Needs to be overwritten. This provides flexibility for the choice of ERC20 implementation used. Must revert if allowance is not sufficient.
*/
function _spendAllowance(address sender, address spender, uint256 amount) internal override(ERC20, InterchainTokenStandard) {
uint256 _allowance = allowance(sender, spender);

if (_allowance != UINT256_MAX) {
_approve(sender, spender, _allowance - amount);
}
}
}
63 changes: 63 additions & 0 deletions examples/evm/its-lock-unlock-fee/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# ITS Lock/Unlock_Fee

This example demonstrates how to use the ITS with a custom token implementation with the `LOCK_UNLOCK_FEE` token manager type.

The token will be minted/burned on transfers. The token needs to give mint permission to the token manager, but burning happens via an approval.

### Prerequisite

Make sure you've already followed the following steps:

- [Setup environment variables](/README.md#set-environment-variables)
- [Run the local chains](/README.md#running-the-local-chains)

### Deployment

To deploy the custom token, use the following command:

```bash
npm run deploy evm/its-lock-unlock-fee [local|testnet]
```

The aforementioned command pertains to specifying the intended environment for a project to execute on. It provides the option to choose between local and testnet environments by appending either `local` or `testnet` after the command.

An example of its usage is demonstrated as follows: `npm run deploy evm/its-lock-unlock-fee local` or `npm run deploy evm/its-lock-unlock-fee testnet`.

### Execution

To execute the example, use the following command:

```bash
npm run execute evm/its-lock-unlock-fee [local|testnet] ${srcChain} ${destChain} ${amount} ${salt}
```

### Parameters

- `srcChain`: The blockchain network from which the message will be relayed. Acceptable values include "Moonbeam", "Avalanche", "Fantom", "Ethereum", and "Polygon". Default value is Avalanche.
- `destChain`: The blockchain network to which the message will be relayed. Acceptable values include "Moonbeam", "Avalanche", "Fantom", "Ethereum", and "Polygon". Default value is Fantom.
- `amount`: The amount of token to send. The default is 1000.
- `salt`: The 32 byte salt to use for the token. The default is a random salt depending on the proccess that runs the example.

## Example

This example deploys the custom token on a local network, registers it with the Interchain Token Service using a `LOCK_UNLOCK_FEE` token manager type and sends 1234 of said token from Fantom to Avalanche but only receives the amount after fee set in the token itself.

```bash
npm run deploy evm/its-lock-unlock-fee local
npm run execute evm/its-lock-unlock-fee local "Fantom" "Avalanche" 1234 "0xa457d6C043b7288454773321a440BA8866D47f96D924D4C38a50b2b0698fae46"
```

The output will be:

```
Registering custom burnable token for Fantom
Approved token for `transferFrom()`
Deploying new manager on dest
Registering custom fee token for Avalanche
Deducting 5% of the tokens sent for the fee
--- Initially ---
Balance at Avalanche is 0
Sending 1234 of token 0x0EBA33C49c72d2907aFFcacFaC0725409E8b3bc1 to Avalanche
--- After ---
Balance at Avalanche is 1173
```
66 changes: 66 additions & 0 deletions examples/evm/its-lock-unlock-fee/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
'use strict';

const {
utils: { deployContract },
} = require('@axelar-network/axelar-local-dev');
const { IInterchainTokenService } = require('@axelar-network/axelar-local-dev/dist/contracts');
const { Contract } = require('ethers');
const { keccak256, defaultAbiCoder } = require('ethers/lib/utils');
const { interchainTransfer } = require('../../../scripts/libs/its-utils');

const FeeToken = rootRequire('./artifacts/examples/evm/its-lock-unlock-fee/FeeToken.sol/FeeToken.json');

const ITokenManager = rootRequire(
'./artifacts/@axelar-network/interchain-token-service/contracts/interfaces/ITokenManager.sol/ITokenManager.json',
);
const LOCK_UNLOCK_FEE = 3;
const MINT_BURN = 4;

const lockFee = (5e16).toString();
async function deploy(chain, wallet) {
console.log(`Deploying Custom Fee Token for ${chain.name}.`);
chain.feeToken = await deployContract(wallet, FeeToken, ['Custom Fee Token', 'CFT', 18, lockFee, chain.interchainTokenService]);
chain.wallet = wallet;
console.log(`Deployed Custom Fee Token for ${chain.name} at ${chain.feeToken.address}.`);
}

async function execute(chains, wallet, options) {
const args = options.args || [];
const { source, destination, calculateBridgeFee } = options;

const amount = args[2] || 1e18;
const salt = args[3] || keccak256(defaultAbiCoder.encode(['uint256', 'uint256'], [process.pid, process.ppid]));

const fee = await calculateBridgeFee(source, destination);
let its;
async function deployTokenManager(chain, tokenManagerType, salt) {
console.log(`Registering custom fee token for ${chain.name}`);
const params = defaultAbiCoder.encode(['bytes', 'address'], [wallet.address, chain.feeToken.address]);
its = new Contract(chain.interchainTokenService, IInterchainTokenService.abi, wallet.connect(chain.provider));
await (await its.deployTokenManager(salt, '', tokenManagerType, params, 0)).wait();
const tokenId = await its.interchainTokenId(wallet.address, salt);
const tokenManagerAddress = await its.tokenManagerAddress(tokenId);
const tokenManager = new Contract(tokenManagerAddress, ITokenManager.abi, wallet.connect(chain.provider));
await (await chain.feeToken.addMinter(tokenManagerAddress)).wait();
return tokenManager;
}

const tokenManager = await deployTokenManager(source, LOCK_UNLOCK_FEE, salt);
const tokenId = await tokenManager.interchainTokenId();

await (await source.feeToken.mint(wallet.address, amount)).wait();
await (await source.feeToken.approve(its.address, amount)).wait();

console.log('Approved token for `transferFrom()`');
console.log('Deploying new manager on dest');
await deployTokenManager(destination, MINT_BURN, salt);
const feeNumber = Number(lockFee);
const percentage = (feeNumber / 1e18) * 100;
console.log(`Deducting ${percentage}% of the tokens sent for the fee`);
await interchainTransfer(source, destination, wallet, tokenId, amount, fee);
}

module.exports = {
deploy,
execute,
};
Loading

0 comments on commit 075dad5

Please sign in to comment.