Contract Pays
A system contract change, which is proposed here, can enable contract-pays on eosio chains. This document explains how the PR enables contract-pays and how contract developers may implement it.
The Approach
Early in eosio history, some on the Eosio Developers chat discussed doing the following:
- Create an account, let's call it
provider
, with a new authority, let's call itpayforit
. - Create a contract on that account with an action, let's call it
acceptcharge
. This action scans the transaction and aborts ifprovider
is unwilling to pay for it. - Use
linkauth
to enableprovider@payforit
to authorizeprovider::acceptcharge
. - Delegate plenty of CPU and NET to
provider
. - Publish the private key of
provider@payforit
.
Usage:
- Anyone could use
provider@payforit
's private key to sign a transaction. - The only thing
provider@payforit
should authorize (see below) isprovider::acceptcharge
. - If
provider::acceptcharge
is the first action, and that action doesn't abort the transaction, thenprovider
will cover NET and CPU costs.
Attack Vectors:
provider@payforit
can authorizeupdateauth
, allowing anyone to replace the published private key with their own, denying access to others.updateauth
also would allow anyone to create a new subauthority underpayforit
, with keys of their choosing.provider@payforit
can authorizelinkauth
, allowing anyone to relinkprovider::acceptcharge
to the new subauthority, or toactive
orowner
.provider@payforit
can authorizeunlinkauth
, allowing anyone to disablepayforit
's access toprovider::acceptcharge
.provider@payforit
can also authorizedeleteauth
.- Since
provider@payforit
's authorization appears within the transaction, an attacker can setdelay
to non-0, consumingprovider
's RAM to store deferred transactions.
Attack Mitigation
A system contract update could block updateauth
, linkauth
, unlinkauth
, and deleteauth
. This covers most of the attack vectors.
To prevent RAM consumption attacks, provider
must not pay for deferred transactions. The easiest way to prevent this is for provider
to have no free RAM. Since provider::acceptcharge
may need free RAM for tracking purposes, the contract should be on a different account than the resource provider account.
Example Code
// This version of eosio::get_action doesn't abort when index is out of range.
std::optional<eosio::action> better_get_action(uint32_t type, uint32_t index)
{
auto size = eosio::internal_use_do_not_use::get_action(type, index, nullptr, 0);
if (size < 0)
return std::nullopt;
std::vector<char> raw(size);
auto size2 = eosio::internal_use_do_not_use::get_action(
type, index, raw.data(), size);
eosio::check(size2 == size, "get_action failed");
return eosio::unpack<eosio::action>(raw.data(), size);
}
// Examine the transaction to see if we're ok accepting the CPU and NET charges
void the_contract::acceptcharge()
{
// type 0: context-free action
// type 1: normal action
for (uint32_t type = 0; type < 2; ++type)
{
for (uint32_t index = 0;; ++index)
{
auto action = better_get_action(type, index);
if (!action)
break;
// Simple rule: only allow actions on this contract
eosio::check(action->account == get_self(),
"This transaction has something I won't pay for");
}
}
}