use crate::{
address::AddressMapper,
test_utils::{builder::Contract, ALICE, DJANGO, DJANGO_ADDR},
tests::{
builder,
test_utils::{get_balance, get_contract_checked},
Contracts, ExtBuilder, RuntimeOrigin, Test,
},
BalanceOf, Code, Config, Pallet, H160,
};
use alloy_core::sol_types::{SolCall, SolConstructor};
use frame_support::traits::fungible::Mutate;
use pallet_revive_fixtures::{
compile_module_with_type, FixtureType, Terminate, TerminateCaller, TerminateDelegator,
};
use pretty_assertions::assert_eq;
use test_case::{test_case, test_matrix};
fn decode_error(output: &[u8]) -> String {
use alloy_core::sol_types::SolError;
alloy_core::sol! { error Error(string); }
Error::abi_decode_validate(output).unwrap().0
}
const METHOD_PRECOMPILE: u8 = 0;
const METHOD_DELEGATE_CALL: u8 = 1;
const METHOD_SYSCALL: u8 = 2;
#[test_matrix(
[FixtureType::Solc, FixtureType::Resolc],
[METHOD_PRECOMPILE, METHOD_SYSCALL]
)]
fn base_case(fixture_type: FixtureType, method: u8) {
let (code, _) = compile_module_with_type("Terminate", fixture_type).unwrap();
ExtBuilder::default().build().execute_with(|| {
let _ = <Test as Config>::Currency::set_balance(&ALICE, 100_000_000_000);
let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(code))
.constructor_data(
Terminate::constructorCall {
skip: true,
method,
beneficiary: DJANGO_ADDR.0.into(),
}
.abi_encode(),
)
.build_and_unwrap_contract();
let result = builder::bare_call(addr)
.data(
Terminate::terminateCall { method, beneficiary: DJANGO_ADDR.0.into() }.abi_encode(),
)
.build_and_unwrap_result();
assert!(result.data.is_empty());
});
}
#[test_case(FixtureType::Solc)]
#[test_case(FixtureType::Resolc)]
fn precompile_fails_in_constructor(fixture_type: FixtureType) {
let (code, _) = compile_module_with_type("Terminate", fixture_type).unwrap();
ExtBuilder::default().build().execute_with(|| {
let _ = <Test as Config>::Currency::set_balance(&ALICE, 100_000_000_000);
let result = builder::bare_instantiate(Code::Upload(code))
.constructor_data(
Terminate::constructorCall {
skip: false,
method: METHOD_PRECOMPILE,
beneficiary: DJANGO_ADDR.0.into(),
}
.abi_encode(),
)
.build_and_unwrap_result();
assert!(result.result.did_revert());
assert_eq!(
decode_error(result.result.data.as_ref()),
"terminate pre-compile cannot be called from the constructor"
);
});
}
#[test_case(FixtureType::Solc)]
#[test_case(FixtureType::Resolc)]
fn syscall_passes_in_constructor(fixture_type: FixtureType) {
let (code, _) = compile_module_with_type("Terminate", fixture_type).unwrap();
ExtBuilder::default().build().execute_with(|| {
let _ = <Test as Config>::Currency::set_balance(&ALICE, 100_000_000_000);
let result = builder::bare_instantiate(Code::Upload(code))
.constructor_data(
Terminate::constructorCall {
skip: false,
method: METHOD_SYSCALL,
beneficiary: DJANGO_ADDR.0.into(),
}
.abi_encode(),
)
.build_and_unwrap_result();
assert!(!result.result.did_revert());
assert!(result.result.data.is_empty());
});
}
#[test_case(FixtureType::Solc)]
#[test_case(FixtureType::Resolc)]
fn precompile_fails_for_direct_delegate(fixture_type: FixtureType) {
let (code, _) = compile_module_with_type("Terminate", fixture_type).unwrap();
ExtBuilder::default().build().execute_with(|| {
let _ = <Test as Config>::Currency::set_balance(&ALICE, 100_000_000_000);
let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(code))
.constructor_data(
Terminate::constructorCall {
skip: true,
method: METHOD_PRECOMPILE,
beneficiary: DJANGO_ADDR.0.into(),
}
.abi_encode(),
)
.build_and_unwrap_contract();
let result = builder::bare_call(addr)
.data(
Terminate::terminateCall {
method: METHOD_DELEGATE_CALL,
beneficiary: DJANGO_ADDR.0.into(),
}
.abi_encode(),
)
.build_and_unwrap_result();
assert!(result.did_revert());
assert_eq!(
decode_error(result.data.as_ref()),
"illegal to call this pre-compile via delegate call",
);
});
}
#[test_case(FixtureType::Solc)]
#[test_case(FixtureType::Resolc)]
fn precompile_fails_for_indirect_delegate(fixture_type: FixtureType) {
let (code, _) = compile_module_with_type("Terminate", fixture_type).unwrap();
ExtBuilder::default().build().execute_with(|| {
let _ = <Test as Config>::Currency::set_balance(&ALICE, 100_000_000_000);
let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(code))
.constructor_data(
Terminate::constructorCall {
skip: true,
method: METHOD_PRECOMPILE,
beneficiary: DJANGO_ADDR.0.into(),
}
.abi_encode(),
)
.build_and_unwrap_contract();
let result = builder::bare_call(addr)
.data(
Terminate::indirectDelegateTerminateCall { beneficiary: DJANGO_ADDR.0.into() }
.abi_encode(),
)
.build_and_unwrap_result();
assert!(result.did_revert());
assert_eq!(
decode_error(result.data.as_ref()),
"illegal to call this pre-compile via delegate call",
);
});
}
#[test_case(FixtureType::Solc)]
#[test_case(FixtureType::Resolc)]
fn syscall_passes_for_direct_delegate_same_tx(fixture_type: FixtureType) {
let (code, _) = compile_module_with_type("Terminate", fixture_type).unwrap();
let (caller_code, _) = compile_module_with_type("TerminateCaller", fixture_type).unwrap();
let (delegator_code, _) = compile_module_with_type("TerminateDelegator", fixture_type).unwrap();
ExtBuilder::default().build().execute_with(|| {
let min_balance = Contracts::min_balance();
let _ = <Test as Config>::Currency::set_balance(&ALICE, 100_000_000_000);
if fixture_type == FixtureType::Resolc {
let _ = <Pallet<Test>>::upload_code(
RuntimeOrigin::signed(ALICE.clone()),
code.clone(),
<BalanceOf<Test>>::MAX,
);
let _ = <Pallet<Test>>::upload_code(
RuntimeOrigin::signed(ALICE.clone()),
delegator_code.clone(),
<BalanceOf<Test>>::MAX,
);
}
let Contract { addr: caller_addr, .. } =
builder::bare_instantiate(Code::Upload(caller_code))
.native_value(125)
.build_and_unwrap_contract();
let result = builder::bare_call(caller_addr)
.data(
TerminateCaller::delegateCallTerminateCall {
value: alloy_core::primitives::U256::from(123_000_000u64),
method: METHOD_SYSCALL,
beneficiary: DJANGO_ADDR.0.into(),
}
.abi_encode(),
)
.build_and_unwrap_result();
assert!(!result.did_revert());
let decoded =
TerminateCaller::delegateCallTerminateCall::abi_decode_returns(&result.data).unwrap();
let addr = H160::from_slice(decoded._1.as_slice());
let delegator_addr = H160::from_slice(decoded._0.as_slice());
assert_eq!(
get_balance(&DJANGO),
123 + min_balance,
"unexpected django balance after terminate"
);
assert!(
get_contract_checked(&addr).is_some(),
"Terminate contract should still exist after terminate"
);
assert!(
get_contract_checked(&delegator_addr).is_none(),
"TerminateDelegator contract should not exist after terminate"
);
});
}
#[test_case(FixtureType::Solc)]
#[test_case(FixtureType::Resolc)]
fn syscall_passes_for_direct_delegate(fixture_type: FixtureType) {
let (code, _) = compile_module_with_type("Terminate", fixture_type).unwrap();
let (delegator_code, _) = compile_module_with_type("TerminateDelegator", fixture_type).unwrap();
ExtBuilder::default().build().execute_with(|| {
let min_balance = Contracts::min_balance();
let _ = <Test as Config>::Currency::set_balance(&ALICE, 100_000_000_000);
let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(code.clone()))
.constructor_data(
Terminate::constructorCall {
skip: true,
method: METHOD_SYSCALL,
beneficiary: DJANGO_ADDR.0.into(),
}
.abi_encode(),
)
.build_and_unwrap_contract();
let Contract { addr: delegator_addr, .. } =
builder::bare_instantiate(Code::Upload(delegator_code.clone()))
.native_value(123)
.build_and_unwrap_contract();
let account_delegator = <Test as Config>::AddressMapper::to_account_id(&delegator_addr);
let result = builder::bare_call(delegator_addr)
.data(
TerminateDelegator::delegateCallTerminateCall {
terminate_addr: addr.0.into(),
method: METHOD_SYSCALL,
beneficiary: DJANGO_ADDR.0.into(),
}
.abi_encode(),
)
.build_and_unwrap_result();
assert!(!result.did_revert());
assert_eq!(
get_balance(&DJANGO),
123 + min_balance,
"unexpected django balance after terminate"
);
assert_eq!(
get_balance(&account_delegator),
min_balance,
"unexpected delegator balance after terminate"
);
assert!(
get_contract_checked(&addr).is_some(),
"Terminate contract should still exist after terminate"
);
assert!(
get_contract_checked(&delegator_addr).is_some(),
"TerminateDelegator contract should still exist after terminate"
);
});
}
#[test_matrix(
[FixtureType::Solc, FixtureType::Resolc],
[FixtureType::Solc, FixtureType::Resolc]
)]
fn terminate_shall_rollback_if_subsequent_frame_fails(
caller_type: FixtureType,
callee_type: FixtureType,
) {
let (code, _) = compile_module_with_type("Terminate", callee_type).unwrap();
let (caller_code, _) = compile_module_with_type("TerminateCaller", caller_type).unwrap();
ExtBuilder::default().build().execute_with(|| {
let min_balance = Contracts::min_balance();
let _ = <Test as Config>::Currency::set_balance(&ALICE, 100_000_000_000);
let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(code))
.constructor_data(
Terminate::constructorCall {
skip: true,
method: METHOD_PRECOMPILE,
beneficiary: DJANGO_ADDR.0.into(),
}
.abi_encode(),
)
.build_and_unwrap_contract();
let account = <Test as Config>::AddressMapper::to_account_id(&addr);
assert!(get_contract_checked(&addr).is_some(), "contract does not exist after create");
assert_eq!(get_balance(&account), min_balance, "unexpected contract balance after create");
let Contract { addr: caller_addr, .. } =
builder::bare_instantiate(Code::Upload(caller_code))
.native_value(125)
.build_and_unwrap_contract();
let caller_account = <Test as Config>::AddressMapper::to_account_id(&caller_addr);
let result = builder::bare_call(caller_addr)
.data(
TerminateCaller::revertAfterTerminateCall {
terminate_addr: addr.0.into(),
method: METHOD_PRECOMPILE,
beneficiary: DJANGO_ADDR.0.into(),
}
.abi_encode(),
)
.build_and_unwrap_result();
assert!(result.did_revert(), "revertAfterTerminateCall did not revert");
assert!(
get_contract_checked(&addr).is_some(),
"contract does not exist after reverted terminate"
);
assert_eq!(
get_balance(&account),
min_balance,
"unexpected contract balance after reverted terminate"
);
assert_eq!(get_balance(&DJANGO), 0, "unexpected DJANGO balance after reverted terminate");
assert_eq!(
get_balance(&caller_account),
125 + min_balance,
"unexpected caller balance after reverted terminate"
);
});
}
#[test_matrix(
[FixtureType::Solc, FixtureType::Resolc],
[METHOD_PRECOMPILE, METHOD_SYSCALL]
)]
fn sent_funds_after_terminate_shall_be_credited_to_beneficiary_base_case(
fixture_type: FixtureType,
method: u8,
) {
let (code, _) = compile_module_with_type("Terminate", fixture_type).unwrap();
let (caller_code, _) = compile_module_with_type("TerminateCaller", fixture_type).unwrap();
ExtBuilder::default().build().execute_with(|| {
let min_balance = Contracts::min_balance();
let _ = <Test as Config>::Currency::set_balance(&ALICE, 100_000_000_000);
if fixture_type == FixtureType::Resolc {
let _ = <Pallet<Test>>::upload_code(
RuntimeOrigin::signed(ALICE.clone()),
code.clone(),
<BalanceOf<Test>>::MAX,
);
}
let Contract { addr: caller_addr, .. } =
builder::bare_instantiate(Code::Upload(caller_code))
.native_value(125)
.build_and_unwrap_contract();
let caller_account = <Test as Config>::AddressMapper::to_account_id(&caller_addr);
assert_eq!(
get_balance(&caller_account),
125 + min_balance,
"unexpected caller balance before terminate"
);
let result = builder::bare_call(caller_addr)
.data(
TerminateCaller::sendFundsAfterTerminateAndCreateCall {
value: alloy_core::primitives::U256::from(123_000_000u64),
method,
beneficiary: DJANGO_ADDR.0.into(),
}
.abi_encode(),
)
.build_and_unwrap_result();
assert!(
!result.did_revert(),
"sendFundsAfterTerminateAndCreateCall reverted: {}",
decode_error(&result.data)
);
let decoded =
TerminateCaller::sendFundsAfterTerminateAndCreateCall::abi_decode_returns(&result.data)
.unwrap();
let addr = H160::from_slice(decoded.0.as_slice());
assert!(get_contract_checked(&addr).is_none(), "contract still exists after terminate");
assert_eq!(
get_balance(&DJANGO),
123 + min_balance,
"unexpected DJANGO balance after terminate"
);
let account = <Test as Config>::AddressMapper::to_account_id(&addr);
assert_eq!(get_balance(&account), 0, "ucontract has balance after terminate");
});
}
#[test_matrix(
[FixtureType::Solc, FixtureType::Resolc],
[FixtureType::Solc, FixtureType::Resolc]
)]
fn sent_funds_after_terminate_shall_be_credited_to_beneficiary_precompile(
caller_type: FixtureType,
callee_type: FixtureType,
) {
let (code, _) = compile_module_with_type("Terminate", callee_type).unwrap();
let (caller_code, _) = compile_module_with_type("TerminateCaller", caller_type).unwrap();
ExtBuilder::default().build().execute_with(|| {
let min_balance = Contracts::min_balance();
let _ = <Test as Config>::Currency::set_balance(&ALICE, 100_000_000_000);
let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(code))
.constructor_data(
Terminate::constructorCall {
skip: true,
method: METHOD_PRECOMPILE,
beneficiary: DJANGO_ADDR.0.into(),
}
.abi_encode(),
)
.build_and_unwrap_contract();
let account = <Test as Config>::AddressMapper::to_account_id(&addr);
assert!(get_contract_checked(&addr).is_some(), "contract does not exist after create");
assert_eq!(get_balance(&account), min_balance, "unexpected contract balance after create");
let Contract { addr: caller_addr, .. } =
builder::bare_instantiate(Code::Upload(caller_code))
.native_value(125)
.build_and_unwrap_contract();
let caller_account = <Test as Config>::AddressMapper::to_account_id(&caller_addr);
assert_eq!(
get_balance(&caller_account),
125 + min_balance,
"unexpected caller balance before terminate"
);
let result = builder::bare_call(caller_addr)
.data(
TerminateCaller::sendFundsAfterTerminateCall {
terminate_addr: addr.0.into(),
value: alloy_core::primitives::U256::from(123_000_000u64),
method: METHOD_PRECOMPILE,
beneficiary: DJANGO_ADDR.0.into(),
}
.abi_encode(),
)
.build_and_unwrap_result();
assert!(
!result.did_revert(),
"sendFundsAfterTerminateCall reverted: {}",
decode_error(&result.data)
);
assert!(
result.data.is_empty(),
"sendFundsAfterTerminateCall returned unexpected data: {:?}",
result.data
);
assert!(get_contract_checked(&addr).is_none(), "contract still exists after terminate");
assert_eq!(
get_balance(&DJANGO),
123 + min_balance,
"unexpected DJANGO balance after terminate"
);
assert_eq!(get_balance(&account), 0, "contract has balance after terminate");
});
}
#[test_matrix(
[FixtureType::Solc, FixtureType::Resolc],
[FixtureType::Solc, FixtureType::Resolc]
)]
fn sent_funds_after_terminate_shall_not_be_credited_to_beneficiary_syscall(
caller_type: FixtureType,
callee_type: FixtureType,
) {
let (code, _) = compile_module_with_type("Terminate", callee_type).unwrap();
let (caller_code, _) = compile_module_with_type("TerminateCaller", caller_type).unwrap();
ExtBuilder::default().build().execute_with(|| {
let min_balance = Contracts::min_balance();
let _ = <Test as Config>::Currency::set_balance(&ALICE, 100_000_000_000);
let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(code))
.constructor_data(
Terminate::constructorCall {
skip: true,
method: METHOD_PRECOMPILE,
beneficiary: DJANGO_ADDR.0.into(),
}
.abi_encode(),
)
.build_and_unwrap_contract();
let account = <Test as Config>::AddressMapper::to_account_id(&addr);
assert!(get_contract_checked(&addr).is_some(), "contract does not exist after create");
assert_eq!(get_balance(&account), min_balance, "unexpected contract balance after create");
let Contract { addr: caller_addr, .. } =
builder::bare_instantiate(Code::Upload(caller_code))
.native_value(125)
.build_and_unwrap_contract();
let caller_account = <Test as Config>::AddressMapper::to_account_id(&caller_addr);
assert_eq!(
get_balance(&caller_account),
125 + min_balance,
"unexpected caller balance before terminate"
);
let result = builder::bare_call(caller_addr)
.data(
TerminateCaller::sendFundsAfterTerminateCall {
terminate_addr: addr.0.into(),
value: alloy_core::primitives::U256::from(123_000_000u64),
method: METHOD_SYSCALL,
beneficiary: DJANGO_ADDR.0.into(),
}
.abi_encode(),
)
.build_and_unwrap_result();
assert!(
!result.did_revert(),
"sendFundsAfterTerminateCall reverted: {}",
decode_error(&result.data)
);
assert!(
result.data.is_empty(),
"sendFundsAfterTerminateCall returned unexpected data: {:?}",
result.data
);
assert!(get_contract_checked(&addr).is_some(), "contract does not exist after terminate");
assert_eq!(get_balance(&DJANGO), 0, "unexpected DJANGO balance after terminate");
assert_eq!(
get_balance(&account),
123 + min_balance,
"unexpected contract balance after terminate"
);
});
}
#[test_matrix(
[FixtureType::Solc, FixtureType::Resolc],
[METHOD_SYSCALL, METHOD_PRECOMPILE],
[METHOD_SYSCALL, METHOD_PRECOMPILE]
)]
fn terminate_twice(fixture_type: FixtureType, method1: u8, method2: u8) {
let (code, _) = compile_module_with_type("Terminate", fixture_type).unwrap();
let (caller_code, _) = compile_module_with_type("TerminateCaller", fixture_type).unwrap();
ExtBuilder::default().build().execute_with(|| {
let min_balance = Contracts::min_balance();
let _ = <Test as Config>::Currency::set_balance(&ALICE, 100_000_000_000);
if fixture_type == FixtureType::Resolc {
let _ = <Pallet<Test>>::upload_code(
RuntimeOrigin::signed(ALICE.clone()),
code.clone(),
<BalanceOf<Test>>::MAX,
);
}
let Contract { addr: caller_addr, .. } =
builder::bare_instantiate(Code::Upload(caller_code))
.native_value(125)
.build_and_unwrap_contract();
let result = builder::bare_call(caller_addr)
.data(
TerminateCaller::createAndTerminateTwiceCall {
value: alloy_core::primitives::U256::from(123_000_000u64),
method1,
method2,
beneficiary: DJANGO_ADDR.0.into(),
}
.abi_encode(),
)
.build_and_unwrap_result();
assert!(
!result.did_revert(),
"createAndTerminateTwiceCall reverted: {}",
decode_error(&result.data)
);
let decoded =
TerminateCaller::createAndTerminateTwiceCall::abi_decode_returns(&result.data).unwrap();
let addr = H160::from_slice(decoded.0.as_slice());
let account = <Test as Config>::AddressMapper::to_account_id(&addr);
assert!(get_contract_checked(&addr).is_none(), "contract still exists after terminate");
assert_eq!(get_balance(&account), 0, "unexpected contract balance after terminate");
assert_eq!(
get_balance(&DJANGO),
123 + min_balance,
"unexpected DJANGO balance after terminate"
);
});
}
#[test_matrix(
[FixtureType::Solc, FixtureType::Resolc],
[METHOD_SYSCALL, METHOD_PRECOMPILE]
)]
fn call_after_terminate_works(fixture_type: FixtureType, method: u8) {
let (code, _) = compile_module_with_type("Terminate", fixture_type).unwrap();
let (caller_code, _) = compile_module_with_type("TerminateCaller", fixture_type).unwrap();
ExtBuilder::default().build().execute_with(|| {
let _ = <Test as Config>::Currency::set_balance(&ALICE, 100_000_000_000);
let expected_value = alloy_core::primitives::U256::from(0xDEADBEEFu64);
if fixture_type == FixtureType::Resolc {
let _ = <Pallet<Test>>::upload_code(
RuntimeOrigin::signed(ALICE.clone()),
code.clone(),
<BalanceOf<Test>>::MAX,
);
}
let Contract { addr: caller_addr, .. } =
builder::bare_instantiate(Code::Upload(caller_code)).build_and_unwrap_contract();
let result = builder::bare_call(caller_addr)
.data(
TerminateCaller::callAfterTerminateCall { value: expected_value, method }
.abi_encode(),
)
.build_and_unwrap_result();
assert!(
!result.did_revert(),
"callAfterTerminateCall reverted: {}",
decode_error(&result.data)
);
let decoded =
TerminateCaller::callAfterTerminateCall::abi_decode_returns(&result.data).unwrap();
let addr = H160::from_slice(decoded._0.as_slice());
let account = <Test as Config>::AddressMapper::to_account_id(&addr);
let value = decoded._1;
assert_eq!(value, expected_value, "unexpected return value from callAfterTerminateCall");
assert_eq!(get_balance(&account), 0, "unexpected contract balance after terminate");
assert_eq!(get_balance(&DJANGO), 0, "unexpected DJANGO balance after terminate");
});
}