neo-devpack-solidity 0.22.0

Production-focused Solidity-to-NeoVM compilation system
Documentation
#[test]
fn mapping_code_generation_emits_storage_ops() {
    let source = r#"
    pragma solidity ^0.8.19;

    contract MappingExample {
        mapping(address => uint256) public balances;

        function setBalance(address owner, uint256 amount) public {
            balances[owner] = amount;
        }

        function getBalance(address owner) public view returns (uint256) {
            return balances[owner];
        }
    }
    "#;

    let mut metadata = analyse_source(source).expect("analysis failed");
    let ir_module = ir::Module::from_contract(&metadata).expect("IR lowering failed");
    let bytecode = generate_contract_bytecode(&mut metadata, &ir_module, false, 2, false)
        .expect("bytecode generation")
        .script;

    assert!(!bytecode.is_empty());

    // Mapping slots are hashed via CryptoLib.keccak256 (native contract) invoked
    // through System.Contract.Call.
    let call_id = interop_id_bytes("System.Contract.Call");
    assert!(bytecode.windows(4).any(|window| window == call_id));

    const CRYPTOLIB_HASH_LE: [u8; 20] = [
        0x1B, 0xF5, 0x75, 0xAB, 0x11, 0x89, 0x68, 0x84, 0x13, 0x61, 0x0A, 0x35, 0xA1, 0x28, 0x86,
        0xCD, 0xE0, 0xB6, 0x6C, 0x72,
    ];
    let mut cryptolib_push = vec![0x0C, 0x14];
    cryptolib_push.extend_from_slice(&CRYPTOLIB_HASH_LE);
    assert!(
        bytecode
            .windows(cryptolib_push.len())
            .any(|window| window == cryptolib_push.as_slice()),
        "expected CryptoLib script hash to be pushed for keccak256"
    );

    let put_id = interop_id_bytes("System.Storage.Put");
    assert!(bytecode.windows(4).any(|window| window == put_id));
    let get_id = interop_id_bytes("System.Storage.Get");
    assert!(bytecode.windows(4).any(|window| window == get_id));
}

#[test]
fn event_emission_places_name_first_in_payload() {
    // `emit Ping(1, 2)` lowers to the EVM-canonical shape, but with a
    // testnet-safe twist: the eventName slot carries the human-readable
    // name "Ping" (UTF-8 valid, matches manifest), while topic[0] (the
    // signature keccak) is moved into the FIRST element of the state
    // array. This avoids Neo's RPC/log layer faulting when it tries to
    // UTF-8-decode keccak hashes whose first byte isn't valid (e.g. 0xDD
    // for `Transfer(address,address,uint256)`).
    //
    // Generated bytecode (illustrative):
    //   push "Ping"                           (eventName)
    //   push keccak("Ping(uint256,uint256)")  (state[0] = topic0)
    //   push abi.encode(1, 2)                 (state[1] = data)
    //   PACK/REVERSE/SWAP -> (stateArray, eventName)
    //   System.Runtime.Notify
    let source = r#"
    pragma solidity ^0.8.19;

    contract EventOrder {
        event Ping(uint256 a, uint256 b);

        function fire() public {
            emit Ping(1, 2);
        }
    }
    "#;

    let mut metadata = analyse_source(source).expect("analysis failed");
    let ir_module = ir::Module::from_contract(&metadata).expect("IR lowering failed");
    let bytecode = generate_contract_bytecode(&mut metadata, &ir_module, false, 2, false)
        .expect("bytecode generation")
        .script;

    let notify_id = interop_id_bytes("System.Runtime.Notify");
    let notify_sequence: Vec<u8> = std::iter::once(0x41u8).chain(notify_id).collect();
    assert!(
        bytecode
            .windows(notify_sequence.len())
            .any(|window| window == notify_sequence),
        "expected Runtime.Notify syscall"
    );

    // The event name "Ping" is pushed as a 4-byte PUSHDATA1 literal.
    let mut expected_name_push = vec![0x0C, 0x04];
    expected_name_push.extend_from_slice(b"Ping");
    assert!(
        bytecode
            .windows(expected_name_push.len())
            .any(|window| window == expected_name_push.as_slice()),
        "expected event name 'Ping' to be pushed as eventName arg of Runtime.Notify"
    );

    // The topic0 keccak is carried as state[0], not as the Notify eventName.
    // This preserves UTF-8-safe Neo event names while still giving the runtime
    // enough structure to reconstruct Ethereum-style logs.
    use sha3::{Digest, Keccak256};
    let topic0 = Keccak256::digest(b"Ping(uint256,uint256)");
    let mut keccak_push = vec![0x0C, 0x20];
    keccak_push.extend_from_slice(&topic0);
    assert!(
        bytecode
            .windows(keccak_push.len())
            .any(|window| window == keccak_push.as_slice()),
        "keccak256(\"Ping(...)\") must appear as the first state-array topic"
    );
}

#[test]
fn modifiers_are_expanded_into_function_body() {
    let source = r#"
    pragma solidity ^0.8.19;

    contract Modifiers {
        modifier onlyPositive(uint256 minValue) {
            require(minValue > 0, "minValue must be > 0");
            _;
        }

        function f(uint256 x) public onlyPositive(x) {
            // empty body
        }
    }
    "#;

    let metadata = analyse_source(source).expect("analysis failed");
    let module = ir::Module::from_contract(&metadata).expect("IR lowering failed");
    let f = module
        .functions
        .iter()
        .find(|function| function.name == "f")
        .expect("expected f function");

    let instrs = &f.basic_blocks[0].instructions;
    assert!(
        instrs
            .iter()
            .any(|instr| matches!(instr, ir::Instruction::Throw)),
        "expected expanded modifier require() to emit a Throw"
    );
}