paradigmxyz/revmc

compiling multiple smart contracts?

0xSt1ng3R opened this issue · 12 comments

it will be really helpful to see an example for compiling (and running) several smart contracts that call each other - e.g univ2 router calling univ2 pool etc.
tried doing it myself - w/o success, I keep getting "cannot compile more functions after finalizing the module" or segfaults.

thank you.

You'd have to add each bytecode to the module individually using translate, keeping track of the IDs and then calling jit_function for each ID, example

let jit_matrix = [
("default", (true, true)),
("no_gas", (false, true)),
// ("no_stack", (true, false)),
// ("no_gas_no_stack", (false, false)),
];
let jit_ids = jit_matrix.map(|(name, (gas, stack))| {
compiler.gas_metering(gas);
unsafe { compiler.stack_bound_checks(stack) };
(name, compiler.translate(None, bytecode, SPEC_ID).expect(name))
});
for &(name, fn_id) in &jit_ids {
let jit = compiler.jit_function(fn_id).expect(name);
g.bench_function(&format!("revmc/{name}"), |b| b.iter(|| call_jit(jit)));
}

A module finalizes once you JIT a function or emit an object, after that it has to be created again if you want to add more functions to it

thank you! :)
got partially successful result.

now i'm getting:

Transaction result: Call { inputs: CallInputs { input: 0x0902f1ac, return_memory_offset: 509..605, gas_limit: 982136, bytecode_address: 0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc, target_address: 0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc, caller: 0x7a250d5630b4cf539739df2c5dacb4c659f2488d, value: Transfer(0), scheme: StaticCall, is_static: true, is_eof: false } }

seems that i'm doing something wrong with compiling them together? (i'm trying to simulate swapExactTokensForTokens)

that's the code more or less (all the rest of the code, e.g storage creation, works good with revm):

...

let uniswap_v2_router_bytecode = Bytes::from(hex::decode(include_str!("../uniswap_v2_router.hex").trim_start_matches("0x")).unwrap());
let uniswap_v2_pool_bytecode = Bytes::from(hex::decode(include_str!("../uniswap_v2_pool.hex").trim_start_matches("0x")).unwrap());
let erc20_bytecode = Bytes::from(hex::decode(include_str!("../erc20.hex").trim_start_matches("0x")).unwrap());

...

let router_id = compiler.translate(None, &uniswap_v2_router_bytecode, SpecId::LONDON)?;
let _pool_id = compiler.translate(None, &uniswap_v2_pool_bytecode, SpecId::LONDON)?;
let _token_id = compiler.translate(None, &erc20_bytecode, SpecId::LONDON)?;

...

let router_fn = compiler.jit_function(router_id)?;
let _pool_fn = compiler.jit_function(_pool_id)?;
let _token_fn = compiler.jit_function(_token_id)?;

...

let contract = Contract {
    bytecode: revmc::interpreter::analysis::to_analysed(revmc::primitives::Bytecode::new_raw(uniswap_v2_router_bytecode.clone())),
    hash: Some(B256::from(keccak256(&uniswap_v2_router_bytecode))),
    target_address: router_address,
    caller: fake_address,
    call_value: U256::ZERO,
    input: create_uniswap_router_request(
        U256::from(10).pow(U256::from(18)),
        U256::ZERO,
        &[weth_address, usdc_address],
        fake_address,
        U256::from(21000000)
    ),
};

let mut interpreter = Interpreter::new(contract.clone(), 1_000_000, false);
let mut memory = SharedMemory::new();

let result = unsafe { router_fn.call_with_interpreter_and_memory(&mut interpreter, &mut memory, &mut external_context) };

what is wrong ? if you mean that you're expecting the call to happen then you want to use Evm, please look at the examples

works now! thank you!
I did a quick benchmark simulating a swap via the univ2 router with one pool and two tokens. It takes ~130µs in revm and ~141µs in revmc (average between 100 simulations).
why and how is that? one could argue that compilation should give faster runtime...

Can you please share the code?

sure - I made the repo public
https://github.com/0xSt1ng3R/revmc_uniswap_tests
I really hope I broke something :)

Yes, quite a few things actually

  • you're setting code hash to zero when you're inserting the accounts, which messes with the internal revm state, meaning the compiled function is never called
  • if you fix this, you are calling a function that does not exist anymore, because the JITted function is freed when the compiler is dropped, which results in a segfault

Also I recommend you look into using revm::primitives::{address, keccak256} (coming from alloy_primitives) and sol! to heavily simplify your code :)

wow nice!! execution time dropped to ~80µs!!
would it be possible to achieve even faster times?
one anomaly - the first run always takes x2-3 slower... why is that?

i missed completely the things you pointed out - but for my spouse, the code did work? how's that possible? the output was correct...
noted about alloy_primitives, thanks!

the first run is slower because the CPU, caches, etc are not warmed up, I would recommend using a benchmark framework if you want more precision like criterion

your code worked because it was just using revm normally

will do that!
any recommendations on built-in performance related upgrades? like maybe turning off gas_metering or stack_bound_checks and such?

Yeah, you can check out the methods on EvmCompiler for all the configuration available.