Potential frontrunning in orderbook create
Description
The book::create_orderbook
function calls account::create_resource_account
. The latter takes a signer and a seed to calculate an address and then creates an account at that address. This behavior is shown in the following snippet:
let seed_guid = account::create_guid(account);
let seed = bcs::to_bytes<GUID>(&seed_guid);
let (book_signer, book_signer_cap) = account::create_resource_account(account, seed);
If the address of the signer and the seed are known, the address that account::create_resource_account
will use can be determined. Therefore, an attacker can front-run book::create_orderbook
by creating an account at the right address, causing book::create_orderbook
to revert.
The seed and address are trivial to determine; an address is public information and the seed is simply the guid_creation_num
member of the Account
struct. Therefore, the seed can be read from the blockchain.
Impact
Affected users will not be allowed to create orderbooks, which will result in them not being able to use the market.
The following unit test demonstrates how an attacker could front-run book::create_orderbook
:
#[test(account = @dex)]
#[expected_failure]
fun create_fake_orderbook(account: &signer) {
create_fake_coins(account);
let victim_addr = signer::address_of(account);
let guid_creation_num = account::get_guid_next_creation_num(victim_addr);
let seed_id = guid::create_id(victim_addr, guid_creation_num);
let seed_guid = GUID {
id: seed_id
};
let seed = bcs::to_bytes<GUID>(&seed_guid);
let new_addr = account::create_resource_address(&victim_addr, seed);
aptos_account::create_account(new_addr);
// Should fail
book::create_orderbook<FakeBaseCoin, FakeQuoteCoin>(account, 3, 3, 1000);
}
We have provided the full PoC to Laminar for reproduction and verification.
Recommendations
Consider using a nondeterministic seed to create the resource account.
Remediation
Commit 925e8a4↗ in aptos-core, introduced by Aptos during the audit prevents the front running of resource accounts via an override if an account exists at the resource_addr
.
let resource = if (exists_at(resource_addr)) {
let account = borrow_global<Account>(resource_addr);
assert!(
option::is_none(&account.signer_capability_offer.for),
error::already_exists(ERESOURCE_ACCCOUNT_EXISTS),
);
assert!(
account.sequence_number == 0,
error::invalid_state(EACCOUNT_ALREADY_USED),
);
create_signer(resource_addr)