Skip to content
37 changes: 35 additions & 2 deletions crates/contracts/src/precompiles/account_keychain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
pub use IAccountKeychain::{
IAccountKeychainErrors as AccountKeychainError, IAccountKeychainEvents as AccountKeychainEvent,
authorizeKey_0Call as legacyAuthorizeKeyCall, authorizeKey_1Call as authorizeKeyCall,
getAllowedCallsReturn, getRemainingLimitWithPeriodCall,
getRemainingLimitWithPeriodReturn as getRemainingLimitReturn,
authorizeKey_2Call as authorizeKeyWithNonceCall, getAllowedCallsReturn,
getRemainingLimitWithPeriodCall, getRemainingLimitWithPeriodReturn as getRemainingLimitReturn,
};

crate::sol! {
Expand Down Expand Up @@ -92,6 +92,9 @@ crate::sol! {
uint256 remainingLimit
);

/// Emitted when a TIP-1053 key-authorization nonce is consumed.
event KeyAuthorizationNonceConsumed(address indexed account, bytes32 indexed nonce);

/// Legacy authorize-key entrypoint used before T3.
function authorizeKey(
address keyId,
Expand All @@ -111,6 +114,19 @@ crate::sol! {
KeyRestrictions calldata config
) external;

/// Authorize a new key with a TIP-1053 replay nonce.
/// @dev The nonce must be unused for the caller's account. bytes32(0) is a valid nonce.
function authorizeKey(
address keyId,
SignatureType signatureType,
KeyRestrictions calldata config,
bytes32 nonce
) external;

/// Burn a TIP-1053 key-authorization nonce without authorizing a key.
/// @dev Callable by the account root key or an active access key.
function burnKeyAuthorizationNonce(bytes32 nonce) external;

/// Revoke an authorized key
/// @param publicKey The public key to revoke
function revokeKey(address keyId) external;
Expand Down Expand Up @@ -176,6 +192,9 @@ crate::sol! {
address keyId
) external view returns (bool isScoped, CallScope[] memory scopes);

/// Returns whether a TIP-1053 key-authorization nonce has been consumed.
function isKeyAuthorizationNonceUsed(address account, bytes32 nonce) external view returns (bool);

/// Get the key used in the current transaction
/// @return The keyId used in the current transaction
function getTransactionKey() external view returns (address);
Expand All @@ -194,6 +213,8 @@ crate::sol! {
error SignatureTypeMismatch(uint8 expected, uint8 actual);
error CallNotAllowed();
error InvalidCallScope();
error InvalidKeyAuthorizationNonce();
error KeyAuthorizationNonceAlreadyUsed();
error LegacyAuthorizeKeySelectorChanged(bytes4 newSelector);
}
}
Expand Down Expand Up @@ -266,6 +287,18 @@ impl AccountKeychainError {
Self::InvalidCallScope(IAccountKeychain::InvalidCallScope {})
}

/// Creates an error for a TIP-1053 nonce path that is unavailable for the current hardfork.
pub const fn invalid_key_authorization_nonce() -> Self {
Self::InvalidKeyAuthorizationNonce(IAccountKeychain::InvalidKeyAuthorizationNonce {})
}

/// Creates an error for a TIP-1053 nonce that has already been consumed.
pub const fn key_authorization_nonce_already_used() -> Self {
Self::KeyAuthorizationNonceAlreadyUsed(
IAccountKeychain::KeyAuthorizationNonceAlreadyUsed {},
)
}

/// Creates an error for the legacy authorize-key selector being unavailable on T3+.
pub fn legacy_authorize_key_selector_changed(new_selector: [u8; 4]) -> Self {
Self::LegacyAuthorizeKeySelectorChanged(
Expand Down
79 changes: 74 additions & 5 deletions crates/precompiles/src/account_keychain/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ const T3_ADDED: &[[u8; 4]] = &[
IAccountKeychain::getAllowedCallsCall::SELECTOR,
];
const T3_DROPPED: &[[u8; 4]] = &[IAccountKeychain::getRemainingLimitCall::SELECTOR];
const T5_ADDED: &[[u8; 4]] = &[
IAccountKeychain::authorizeKey_2Call::SELECTOR,
IAccountKeychain::burnKeyAuthorizationNonceCall::SELECTOR,
IAccountKeychain::isKeyAuthorizationNonceUsedCall::SELECTOR,
];

impl Precompile for AccountKeychain {
fn call(&mut self, calldata: &[u8], msg_sender: Address) -> PrecompileResult {
Expand All @@ -30,9 +35,12 @@ impl Precompile for AccountKeychain {

dispatch_call(
calldata,
&[SelectorSchedule::new(TempoHardfork::T3)
.with_added(T3_ADDED)
.with_dropped(T3_DROPPED)],
&[
SelectorSchedule::new(TempoHardfork::T3)
.with_added(T3_ADDED)
.with_dropped(T3_DROPPED),
SelectorSchedule::new(TempoHardfork::T5).with_added(T5_ADDED),
],
IAccountKeychainCalls::abi_decode,
|call| match call {
IAccountKeychainCalls::authorizeKey_0(call) => {
Expand Down Expand Up @@ -69,6 +77,16 @@ impl Precompile for AccountKeychain {
IAccountKeychainCalls::authorizeKey_1(call) => {
mutate_void(call, msg_sender, |sender, c| self.authorize_key(sender, c))
}
IAccountKeychainCalls::authorizeKey_2(call) => {
mutate_void(call, msg_sender, |sender, c| {
self.authorize_key_with_nonce(sender, c)
})
}
IAccountKeychainCalls::burnKeyAuthorizationNonce(call) => {
mutate_void(call, msg_sender, |sender, c| {
self.burn_key_authorization_nonce(sender, c)
})
}
IAccountKeychainCalls::revokeKey(call) => {
mutate_void(call, msg_sender, |sender, c| self.revoke_key(sender, c))
}
Expand Down Expand Up @@ -97,6 +115,9 @@ impl Precompile for AccountKeychain {
IAccountKeychainCalls::getAllowedCalls(call) => {
view(call, |c| self.get_allowed_calls(c))
}
IAccountKeychainCalls::isKeyAuthorizationNonceUsed(call) => {
view(call, |c| self.is_key_authorization_nonce_used(c))
}
IAccountKeychainCalls::getTransactionKey(call) => {
view(call, |c| self.get_transaction_key(c, msg_sender))
}
Expand All @@ -115,15 +136,15 @@ mod tests {
test_util::{assert_full_coverage, check_selector_coverage},
};
use alloy::{
primitives::U256,
primitives::{B256, U256},
sol_types::{SolCall, SolError},
};
use tempo_chainspec::hardfork::TempoHardfork;
use tempo_contracts::precompiles::{UnknownFunctionSelector, legacyAuthorizeKeyCall};

#[test]
fn test_account_keychain_selector_coverage() -> eyre::Result<()> {
let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T5);
StorageCtx::enter(&mut storage, || {
let mut fee_manager = AccountKeychain::new();
let selectors: Vec<_> = IAccountKeychainCalls::SELECTORS
Expand Down Expand Up @@ -348,6 +369,54 @@ mod tests {
})
}

#[test]
fn test_t5_nonce_selectors_rejected_pre_t5() -> eyre::Result<()> {
let account = Address::random();
let nonce = B256::repeat_byte(0x53);

let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T4);
StorageCtx::enter(&mut storage, || {
let mut keychain = AccountKeychain::new();
keychain.initialize()?;

for (selector, calldata) in [
(
IAccountKeychain::authorizeKey_2Call::SELECTOR,
IAccountKeychain::authorizeKey_2Call {
keyId: Address::random(),
signatureType: IAccountKeychain::SignatureType::Secp256k1,
config: KeyRestrictions {
expiry: u64::MAX,
enforceLimits: false,
limits: vec![],
allowAnyCalls: true,
allowedCalls: vec![],
},
nonce,
}
.abi_encode(),
),
(
IAccountKeychain::burnKeyAuthorizationNonceCall::SELECTOR,
IAccountKeychain::burnKeyAuthorizationNonceCall { nonce }.abi_encode(),
),
(
IAccountKeychain::isKeyAuthorizationNonceUsedCall::SELECTOR,
IAccountKeychain::isKeyAuthorizationNonceUsedCall { account, nonce }
.abi_encode(),
),
] {
let result = keychain.call(&calldata, account)?;
assert!(result.is_revert(), "expected T5 selector to revert pre-T5");

let decoded = UnknownFunctionSelector::abi_decode(&result.bytes)?;
assert_eq!(decoded.selector.as_slice(), &selector);
}

Ok(())
})
}

#[test]
fn test_t3_selector_with_malformed_data_returns_unknown_selector_error() -> eyre::Result<()> {
let selector = getRemainingLimitWithPeriodCall::SELECTOR;
Expand Down
Loading
Loading