This project demonstrates how to use the Internet Computer (ICP) as a coprocessor for EVM smart contracts. The coprocessor listens to events emitted by an EVM smart contract, processes them, and optionally sends the results back. This starter project is a proof of concept and should not be used in production environments.
To get a better understanding of how the coprocessor works, make sure you check out the recorded workshops in the Additional Resources section.
The concept of coprocessors originated in computer architecture as a technique to enhance performance. Traditional computers rely on a single central processing unit (CPU) to handle all computations. However, as workloads grew more complex, the CPU became overloaded.
Coprocessors were introduced to offload specific tasks from the CPU to specialized hardware. Similarly, in the EVM ecosystem, smart contracts often face computational constraints. Coprocessors and stateful Layer 2 solutions extend the capabilities of the EVM by offloading specific tasks to more powerful environments.
Read more about coprocessors in the context of Ethereum in the article "A Brief Intro to Coprocessors".
Canister smart contracts on ICP can securely read from EVM smart contracts (using HTTPS Outcalls or the EVM RPC canister) and write to them (using Chain-key Signatures, i.e., Threshold ECDSA). This eliminates the need for additional parties to relay messages between the networks, and no extra work is required on the EVM side to verify computation results as the EVM smart contract just needs to check for the proper sender.
Moreover, canister smart contracts have numerous capabilities that can extend smart contract functionality:
- WASM Runtime, which is more efficient than the EVM and allows programming in Rust, JavaScript, and other traditional languages.
- 400 GiB of memory with low storage costs.
- Long-running computations including AI inference.
- HTTPS Outcalls for interacting with other chains and traditional web services.
- Chain-key signatures for signing transactions for other chains, including Ethereum and Bitcoin.
- Timers for syncing with EVM events and scheduling tasks.
- Unbiasable randomness provided by threshold BLS signatures.
- Ability to serve web content directly from canisters.
- The reverse gas model frees end users from paying for every transaction.
- ~1-2 second finality.
- Multi-block transactions.
To deploy the project locally, run ./deploy.sh
from the project root. This script will:
- Start
anvil
- Start
dfx
- Deploy the EVM contract
- Generate a number of jobs to be processed
- Deploy the coprocessor canister
Check the deploy.sh
script comments for detailed deployment steps.
Ensure Docker and VS Code are installed and running, then click the button below:
Ensure the following are installed on your system:
Run these commands in a new, empty project directory:
git clone https://github.com/letmejustputthishere/chain-fusion-starter.git
cd chain-fusion-starter
This starter project involves multiple canisters working together to process events emitted by an EVM smart contract. The contracts involved are:
- EVM Smart Contract: Emits events such as
NewJob
when specific functions are called. It also handles callbacks from thechain_fusion
canister with the results of the processed jobs. - Chain Fusion Canister (
chain_fusion
): Listens to events emitted by the EVM smart contract, processes them, and sends the results back to the EVM smart contract. - EVM RPC Canister: Facilitates communication between the Internet Computer and EVM-based blockchains by making RPC calls to interact with the EVM smart contract.
The full flow of how these canisters interact can be found in the following sequence diagram:
The contracts/Coprocessor.sol
contract emits a NewJob
event when the newJob
function is called, transferring ETH to the chain_fusion
canister to pay it for job processing and transaction fees (this step is optional and can be customized to fit your use case).
// Function to create a new job
function newJob() public payable {
// Require at least 0.01 ETH to be sent with the call
require(msg.value >= 0.01 ether, "Minimum 0.01 ETH not met");
// Forward the ETH received to the coprocessor address
// to pay for the submission of the job result back to the EVM
// contract.
coprocessor.transfer(msg.value);
// Emit the new job event
emit NewJob(job_id);
// Increment job counter
job_id++;
}
The callback
function writes processed results back to the contract:
function callback(string calldata _result, uint256 _job_id) public {
require(
msg.sender == coprocessor,
"Only the coprocessor can call this function"
);
jobs[_job_id] = _result;
}
For local deployment, see the deploy.sh
script and script/Coprocessor.s.sol
.
The chain_fusion
canister listens to NewJob
events by periodically calling the eth_getLogs
RPC method via the EVM RPC canister. Upon receiving an event, it processes the job and sends the results back to the EVM smart contract via the EVM RPC canister, signing the transaction with threshold ECDSA.
The Job processing logic is in canisters/chain_fusion/src/job.rs
:
pub async fn job(event_source: LogSource, event: LogEntry) {
mutate_state(|s| s.record_processed_log(event_source.clone()));
// because we deploy the canister with topics only matching
// NewJob events we can safely assume that the event is a NewJob.
let new_job_event = NewJobEvent::from(event);
// this calculation would likely exceed an ethereum blocks gas limit
// but can easily be calculated on the IC
let result = fibonacci(20);
// we write the result back to the evm smart contract, creating a signature
// on the transaction with chain key ecdsa and sending it to the evm via the
// evm rpc canister
submit_result(result.to_string(), new_job_event.job_id).await;
println!("Successfully ran job #{:?}", &new_job_event.job_id);
}
All coprocessing logic resides in canisters/chain_fusion/src/job.rs
. Developers can focus on writing jobs to process EVM smart contract events without altering the code for fetching events or sending transactions.
If you want to check that the chain_fusion
canister really processed the events, you can either look at the logs output by running ./deploy.sh
– keep an eye open for the Successfully ran job
message – or you can call the EVM contract to get the results of the jobs. To do this, run:
cast call 0x5fbdb2315678afecb367f032d93f642f64180aa3 "getResult(uint)(string)" <job_id>
where <job_id>
is the ID of the job you want to get the result for. This should always return "6765"
for processed jobs, which is the 20th Fibonacci number, and ""
for unprocessed jobs.
If you want to create more jobs, simply run:
cast send 0x5fbdb2315678afecb367f032d93f642f64180aa3 "newJob()" --private-key=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --value 0.01ether
Note that the Chain Fusion Canister only scrapes logs every 3 minutes, so you may need to wait a few minutes before seeing the new job processed.
The storage.rs
module allows you to store data in stable memory, providing up to 400 GiB of available storage. In this starter template, stable memory can used to store assets that can then be served via HTTP.
To use this feature, you need to uncomment the section in lib.rs
that handles HTTP requests. This enables the canister to serve stored assets. Here is the code snippet to uncomment:
// Uncomment this if you need to serve stored assets from `storage.rs` via HTTP requests
// #[ic_cdk::query]
// fn http_request(req: HttpRequest) -> HttpResponse {
// if let Some(asset) = get_asset(&req.path().to_string()) {
// let mut response_builder = HttpResponseBuilder::ok();
// for (name, value) in asset.headers {
// response_builder = response_builder.header(name, value);
// }
// response_builder
// .with_body_and_content_length(asset.body)
// .build()
// } else {
// HttpResponseBuilder::not_found().build()
// }
// }
By enabling this code, you can serve web content directly from the canister, leveraging the stable memory for storing large amounts of data efficiently.
To send transactions to the EVM, this project uses the ic-evm-utils
crate. This crate provides functionality for constructing, signing and sending transactions to EVM networks, leveraging the evm-rpc-canister-types
crate for data types and constants.
-
sign_eip1559_transaction: This function signs a EIP-1559 transaction.
-
eth_call: This function sends a call to an arbitrary EVM smart contract to read data from it. It constructs a JSON-RPC call to the EVM RPC canister, which then forwards the call to the EVM smart contract.
-
erc20_balance_of: The
erc20_balance_of
function demonstrates how to construct and send a call to an ERC20 contract to query the balance of a specific address. It uses theeth_call
function to send the call and parse the response. You can refer to theerc20_balance_of
function in theeth_call.rs
module to understand how to implement similar read operations for other types of EVM smart contracts. -
send_raw_transaction: This function sends a raw transaction to an EVM smart contract. It constructs a transaction, signs it with the canister's private key, and sends it to the EVM network.
-
transfer_eth: The
transfer_eth
function demonstrates how to transfer ETH from a canister-owned EVM address to another address. It covers creating a transaction, signing it with the canister's private key, and sending it to the EVM network.transfer_eth
uses thesend_raw_transaction
function to send the transaction. -
contract_interaction: The
contract_interaction
function demonstrates how to interact with arbitrary EVM smart contracts. It constructs a transaction based on the desired contract interaction, signs it with the canister's private key, and sends it to the EVM network.contract_interaction
uses thesend_raw_transaction
function to send the transaction. Thesubmit_result
function in this starter project leverages this function to send the results of processed jobs back to the EVM smart contract.
Examples leveraging the chain fusion starter logic:
- On-chain asset and metadata creation for ERC721 NFT contracts
- Ethereum Donations Streamer
- Recurring Transactions on Ethereum
- BTC Price Oracle for EVM Smart Contracts
Build your own use case and share it with the community!
Some ideas you could explore:
- A referral canister that distributes rewards to users based on their interactions with an EVM smart contract
- A ckNFT canister that mints an NFT on the ICP when an EVM helper smart contract emits a
ReceivedNft
, similar to theEthDepositHelper
contract the ckETH minter uses. This could enable users to trade NFTs on the ICP without having to pay gas fees on Ethereum. - Decentralized DCA (dollar cost average) service for decentralized exchanges like Uniswap deployed on EVM chains
- Price oracles for DeFi applications via exchange rate canister
- Prediction market resolution
- Soulbound NFT metadata and assets stored in a canister
- An on-chain managed passive index fund (e.g. top 10 ERC20 tokens traded on Uniswap)
- An on-chain donations stream
For more details and discussions, visit the DFINITY Developer Forum or follow @cryptoschindler on Twitter.