Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Valued express example #176

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
//SPDX-License-Identifier: MIT
// //SPDX-License-Identifier: MIT
pragma solidity 0.8.9;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo? Double comment?


import { AxelarExecutable } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/executable/AxelarExecutable.sol';
import { AxelarExpressExecutable } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/express/AxelarExpressExecutable.sol';

import { IAxelarGateway } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGateway.sol';
import { IERC20 } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IERC20.sol';
import { IAxelarGasService } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGasService.sol';
Expand All @@ -11,8 +12,8 @@ import { Upgradable } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/up
contract CallContractWithTokenExpress is AxelarExpressExecutable {
IAxelarGasService public immutable gasService;

constructor(address gateway_, address gasReceiver_) AxelarExpressExecutable(gateway_) {
gasService = IAxelarGasService(gasReceiver_);
constructor(address _gateway, address _gasReceiver) AxelarExpressExecutable(_gateway) {
gasService = IAxelarGasService(_gasReceiver);
}

function sendToMany(
Expand All @@ -36,8 +37,8 @@ contract CallContractWithTokenExpress is AxelarExpressExecutable {
amount,
msg.sender
);
gateway.callContractWithToken(destinationChain, destinationAddress, payload, symbol, amount);
}
gateway.callContractWithToken(destinationChain, destinationAddress, payload, symbol, amount);
}

function _executeWithToken(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//SPDX-License-Identifier: MIT
pragma solidity 0.8.9;

import { AxelarExecutable } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/executable/AxelarExecutable.sol';
import { AxelarValuedExpressExecutable } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/express/AxelarValuedExpressExecutable.sol';
import { IAxelarGateway } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGateway.sol';
import { IAxelarGasService } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGasService.sol';
import { Upgradable } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/upgradable/Upgradable.sol';
import { MockERC20 } from './mocks/MockERC20.sol';

contract CallContractWithValuedExpress is AxelarValuedExpressExecutable {
IAxelarGasService public immutable gasService;

constructor(address _gateway, address _gasReceiver) AxelarValuedExpressExecutable(_gateway) {
gasService = IAxelarGasService(_gasReceiver);
}

function sendValuedMessage(
string memory _destinationChain,
string memory _destinationAddress,
address _tokenAddr,
uint256 _amount,
address _receiver
) external payable {
require(msg.value > 0, 'insufficient funds');

bytes memory valuedMsg = _deriveMsgValueForNonGatewayTokenValueTransfer(_tokenAddr, _amount, _receiver);

gasService.payNativeGasForContractCall{ value: msg.value }(
address(this),
_destinationChain,
_destinationAddress,
valuedMsg,
msg.sender
);
gateway.callContract(_destinationChain, _destinationAddress, valuedMsg);
}

function _execute(string calldata, string calldata, bytes calldata _payload) internal override {
(address tokenAddress, uint256 value, address reciever) = abi.decode(_payload, (address, uint256, address));
MockERC20(tokenAddress).mint(reciever, value);
}

function contractCallWithTokenValue(
string calldata sourceChain,
string calldata sourceAddress,
bytes calldata payload,
string calldata symbol,
uint256 amount
) public view override returns (address tokenAddress, uint256 value) {}

function contractCallValue(
string calldata,
string calldata,
bytes calldata payload
) public view virtual override returns (address tokenAddress, uint256 value) {
(tokenAddress, value) = abi.decode(payload, (address, uint256));
}

function _deriveMsgValueForNonGatewayTokenValueTransfer(
address _tokenAddr,
uint256 _amount,
address _receiver
) internal pure returns (bytes memory valueToBeTransferred) {
valueToBeTransferred = abi.encode(_tokenAddr, _amount, _receiver);
}
}
52 changes: 52 additions & 0 deletions examples/evm/call-contract-with-valued-express/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Call Contract with Valued Express

This is an example of sending an express transaction where the value is not a cross chain asset transfer using `callContractWithToken()`. Rather the value is in the `payload` of the transaction. Once the payload has been decoded and derived to a specific contract address that value can be transfered to the end receiver.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverse this to make the first sentence of the readme what it IS rather than what it ISN'T.


Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add details about how it works at a high level? An overview? I hope we will also have corresponding updates in the docs?

### Deployment

To deploy the contract, run the following command:

```bash
npm run deploy evm/call-contract-with-valued-express [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/call-contract-with-valued-express local` or `npm run deploy evm/call-contract-with-valued-express testnet`.

### Execution

To execute the example, use the following command:

```bash
npm run execute evm/call-contract-with-valued-express [local|testnet] ${srcChain} ${destChain}
```

**Note**
The GMP Express feature is already lived on our testnet. However, the following conditions need to be met:

- The contract address must be whitelisted by our executor service.

Currently, our whitelisted contract addresses for this example are:

- Avalanche: `0x4E3b6C3d284361Eb4fA9aDE5831eEfF85578b72c`
- Polygon: `0xAb6dAb12aCCAe665A44E69d44bcfC6219A30Dd32`

## Example

```bash
npm run deploy evm/call-contract-with-valued-express local
npm run execute evm/call-contract-with-token local "Avalanche" "Fantom" 100 0xBa86A5719722B02a5D5e388999C25f3333c7A9fb
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this right? Deploying a different contract that we execute?


### Output:

```

--- Initially ---
0xBa86A5719722B02a5D5e388999C25f3333c7A9fb has 100 tokens
--- After ---
0xBa86A5719722B02a5D5e388999C25f3333c7A9fb has 200 tokens

```

```
73 changes: 73 additions & 0 deletions examples/evm/call-contract-with-valued-express/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
const {
utils: { deployContract },
} = require('@axelar-network/axelar-local-dev');
const CallContractWithValuedExpress = rootRequire(
'./artifacts/examples/evm/call-contract-with-valued-express/CallContractWithValuedExpress.sol/CallContractWithValuedExpress.json',
);
const MockERC20 = rootRequire('./artifacts/examples/evm/call-contract-with-valued-express/mocks/MockERC20.sol/MockERC20.json');

async function deploy(chain, wallet) {
console.log(`Deploying Call Contract Valued Express for ${chain.name}.`);
chain.callContractWithValuedExpress = await deployContract(wallet, CallContractWithValuedExpress, [chain.gateway, chain.gasService]);
chain.mockToken = await deployContract(wallet, MockERC20);
chain.wallet = wallet;
console.log(`Deployed CallContractWithValuedExpress for ${chain.name} at ${chain.callContractWithValuedExpress.address}.`);
}

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

// Get the accounts to send to.
const accounts = args.slice(3).length ? args.slice(3) : [wallet.address];

// Calculate the express fee for the bridge.
const expressFee = await calculateBridgeExpressFee(source, destination);

// Get the balance of the first account.
const initialBalance = await destination.mockToken.balanceOf(accounts[0]);

// Get the amount to send.
const amount = Math.floor(parseFloat(args[2])) * 1e6 || 10e6;

async function logAccountBalances() {
for (const account of accounts) {
console.log(`${account} has ${(await destination.mockToken.balanceOf(account)) / 1e6} tokens on ${destination.name}`);
}
}

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

console.log('--- Initially ---');

// Log the balances of the accounts.
await logAccountBalances();

// Send tokens to the distribution contract.
const sendTx = await source.callContractWithValuedExpress.sendValuedMessage(
destination.name,
destination.callContractWithValuedExpress.address,
destination.mockToken.address,
amount,
accounts[0],
{
value: expressFee,
},
);

console.log('Sent valued msg to destination contract:', sendTx.hash);

// Wait for the distribution to complete by checking the balance of the first account.
while ((await destination.mockToken.balanceOf(accounts[0])).eq(initialBalance)) {
await sleep(1000);
}

console.log('--- After ---');
// Log the balances of the accounts.
await logAccountBalances();
}

module.exports = {
deploy,
execute,
};
12 changes: 12 additions & 0 deletions examples/evm/call-contract-with-valued-express/mocks/MockERC20.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

import '@openzeppelin/contracts/token/ERC20/ERC20.sol';

contract MockERC20 is ERC20 {
constructor() ERC20("Mock Token", 'MTK') {}

function mint(address to, uint256 amount) public {
_mint(to, amount);
}
}
3 changes: 2 additions & 1 deletion examples/tests/evm.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const examples = [
'call-contract',
'call-contract-with-token',
'call-contract-with-token-express',
'call-contract-with-valued-express',
'cross-chain-token',
'deposit-address',
'nonced-execution',
Expand Down Expand Up @@ -53,7 +54,7 @@ describe('Verify EVM Examples', function () {
if (example.deploy) {
await deploy('local', chains, wallet, example);
}

await executeEVMExample('local', chains, [], wallet, example);
});
}
Expand Down
7 changes: 1 addition & 6 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion scripts/libs/execute.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ async function executeEVMExample(env, chains, args, wallet, example) {
// Get source and destination chains.
const source = getSourceChain(chains, args, example.sourceChain);
const destination = getDestChain(chains, args, example.destinationChain);

// Listen for GMP events on testnet for printing an Axelarscan link for tracking.
const startBlockNumber = await source.provider.getBlockNumber();
listenForGMPEvent(env, source, startBlockNumber);
Expand Down
Loading