4

I have contractB trigger an inline action:

  /// @abi action
  void send() {
    action( permission_level{ _self, N(active) },
           N(contractA), N(get),
           std::make_tuple( _self )
    ).send();
  }

Now, when contractA receives the get action, as it currently is, the user will simply be contractB--which could mean either the contract triggered the action or the contract account.

I don't want this. I would like for contractA to only allow contractB's contract to send the action. Something like user == contractB.code, but have no idea how to check for this distinction.

EDIT:

I tried having require_auth2( N(contractB), N(eosio.code) ); inside of the contractA action, but got the following error:

Exception Details: 3050000 action_validate_exception: Action validate 
exception inline action's authorizations include a non-existent permission: 
{"actor":"contractB","permission":"eosio.code"}
11
  • one way would be to extend the get() method to contractA:get(account_name account, bool from_contract) and create logic in contractB to send the appropriate flag in std::make_tuple(_self, is_contract), but there may (possibly) be a different way to do this by modifying the EOSIO_ABI macro if you don't mind adjusting the autogenerated .abi file as well
    – confused00
    Commented Jul 22, 2018 at 19:45
  • Wait. This doesn't prevent the user from sending the transaction with the from_contract=true. What I am trying to accomplish is an action that can only be done via the contract.
    – Tyrick
    Commented Jul 23, 2018 at 0:20
  • but if the user pushes an action directly to contractA with from_contract=true, the account_name will be their account and not contractB so contractA can detect this
    – confused00
    Commented Jul 23, 2018 at 7:11
  • 1
    Ah. There is a misunderstanding. I am talking about the user being the owner of the account, contractB. I want to know the difference between the user contractB and the contract, contractB. I want ALL users to have to follow the code of a given contract, and that includes the contract owner as well. As of now, since I can't distinguish between the contract owner and contract in an action, this means the contract owner could bypass the intended contract logic (which should not be possible without a contract update--which would have other implications).
    – Tyrick
    Commented Jul 23, 2018 at 14:37
  • 1
    @Tyrick did you send the action from contractB with permission_level{ _self, N(eosio.code) } as well? Commented Jul 23, 2018 at 19:16

2 Answers 2

15

UPDATE: This is the code I used to experiment with all combinations of permissions I could come up with: https://github.com/andresberrios/permissions_test


After a lot of experimentation, I finally understand how permissions work in EOSIO!

EOS Permission Model - Overview

  • An account can have various permissions (like owner and active), which are represented by an account@permission pair.
  • Permissions (which are like roles) can be linked to specific actions of specific contracts to allow those permissions to execute those actions (linkauth). By default, the owner and active permissions can do anything except active can't change the owner permission.
  • Permissions are controlled by an "authority", which is the multisig configuration of who can give that permission (in other words, who can act under that role).
  • Within this multisig configuration, you can have a combination of public keys and other permissions (account@permission pairs), which makes permissions an intrinsically recursive construct.

Acting as another account

Contracts in general should use require_auth(account) and not require_auth2(account, permission) unless there is a very specific reason to do so. Using require_auth2 to require a specific permission of an account can hinder the configurability of the EOSIO permission system. This is because in general, if actions simply require the auth of an account, then that means they are implicitly requiring one of the following permissions of that account:

  1. The owner permission.
  2. The active permission.
  3. Any other custom permission that the user decided to create for their account in order to give granular authorization to specific contract actions.

Point 3 means that a user can create a permission (as I mentioned, it can be seen as a "role") called for example ramtrader and then use linkauth to authorize that permission to use the eosio::buyram and eosio::sellram system contract actions. This can work with any contract, not only the system contract. This way, when defining the ramtrader permission, users will need to specify an authority for it (a multisig configuration), and this authority could specify that the only object that can act under this permission is accountb@active, for example, giving access to the accountb account to buy and sell RAM for account.

Contract code acting as another account

Now that we understand how to act as another account, we can figure out how to allow a contract's code to act as another account, be it for transferring funds (eosio.token::transfer action) or just calling another contract's actions.

When contracts call inline actions, they are supposed to send the right permissions for that action. If for instance a contract that lives in the contract account would try to buy RAM for account using the funds of account itself, it would need to provide the same permissions that account is required to provide when they buy RAM for themselves manually. If we use the account@active permission, then the contract would need to send that permission in the inline action, and not [email protected] as many of us could end up thinking (the documentation on this is very scarce and confusing). In order for the code in the contract account to be able to provide that permission, first account would have to add authorization for the code of contract to the authority (multisig config) that rules it's account@active permission.

This can be achieved by adding the [email protected] permission to the authority, which is a special permission defined by the EOSIO software to specify that only the contract code of the contract account will be able to act under the permission (role) ruled by that authority. This means that the account@active authority would contain the public key that the owner of that account controls, as well as the [email protected] permission.

This effectively implements what you were looking for: Authorizing a contract's code to act as another account, but not letting the contract's account act as the other account.

If you wanted to let a contract's account act as yourself but not the contract's code, you would have to do the same thing but instead of setting [email protected] you would set contract@active or some other more specific (limited) permission.

Setting up the permission authority

To configure your account to allow [email protected] to act on your behalf, you would need to issue a transaction to the eosio::updateauth action with the properly formatted authority data. One way to do it using cleos is what @confused00 showed in his example:

cleos set account permission <YOUR_ACCOUNT> active '{"threshold": 1,"keys": [{"key": "<YOUR_PUBLIC_KEY>","weight": 1}],"accounts": [{"permission":{"actor":"<CONTRACT_ACCOUNT>","permission":"eosio.code"},"weight":1}]}' owner -p <YOUR_ACCOUNT>

It might be easier to save a data.json file and then put the payload in there and point cleos to it:

data.json

{
  "threshold": 1,
  "keys": [
    {
      "key": "<YOUR_PUBLIC_KEY>",
      "weight": 1
    }
  ],
  "accounts": [
    {
      "permission": {
        "actor": "<CONTRACT_ACCOUNT>",
        "permission": "eosio.code"
      },
      "weight": 1
    }
  ]
}

and then:

cleos set account permission <YOUR_ACCOUNT> active data.json owner -p <YOUR_ACCOUNT>
3
  • what is the working code that works based on this method and does what op is asking for? I'm trying to run different versions to see how it works
    – confused00
    Commented Jul 27, 2018 at 18:49
  • I'll post some code tomorrow when I get back on the laptop. Commented Jul 27, 2018 at 21:06
  • @confused00 I updated the answer to add the link to the code. Commented Jul 27, 2018 at 23:16
3
+50

This seems to work for me:

$ mkdir -p project/sender
$ mkdir project/recipient
$ touch project/sender/sender.cpp
$ touch project/recipient/recipient.cpp

sender.cpp

#include<eosiolib/eosio.hpp>
#include<string>

using std::string;
using eosio::contract;
using eosio::permission_level;
using eosio::action;

class sender : contract {
    public:
        sender (account_name account) : contract(account) {}

        void send(account_name account, string message) {
            action(
                permission_level(account, N(active)),
                N(recipient),
                N(receive),
                message
            ).send();
        }
};

EOSIO_ABI(sender, (send))

recipient.cpp

#include<eosiolib/eosio.hpp>
#include<string>

class recipient : eosio::contract {
    public:
        recipient(account_name account) : eosio::contract(account) {}

        void receive(account_name account, std::string message) {
            require_auth2(account, N(eosio.code));
            eosio::print(message);
        }
};

EOSIO_ABI(recipient, (receive))

cleos & eosiocpp

$ cleos wallet unlock
$ cleos wallet keys
[
    "EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV"
]
$ cleos account create eosio sender EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
$ cleos account create eosio recipient EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
$ eosiocpp -g project/sender/sender.abi project/sender/sender.cpp
$ eosiocpp -o project/sender/sender.wasm project/sender/sender.cpp
$ eosiocpp -g project/recipient/recipient.abi project/recipient/recipient.cpp
$ eosiocpp -o project/recipient/recipient.wasm project/recipient/recipient.cpp
$ cleos set contract sender project/sender
$ cleos set contract recipient project/recipient
$ cleos set account permission sender active '{"threshold": 1,"keys": [{"key": "EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV","weight": 1}],"accounts": [{"permission":{"actor":"sender","permission":"eosio.code"},"weight":1}]}' owner -p sender

Afterwards,

$ cleos push action sender send '["sender", "my_message"]' -p sender

goes through fine, while

$ cleos push action recipient receive '["sender", "my_message"]' -p sender

fails with missing authority.

So this setup allows 2 contracts to communicate between each other, but the recipient only allows messages from contract and not from the account

10
  • Nice solution, I was experimenting with this as well. I noticed you are sending only the message in the inline action call, but the receive action expects 2 params. I proposed an edit to your answer just to add it in there but I'm not really sure if it should be the account variable or _self. Commented Jul 27, 2018 at 0:25
  • Thanks! However, it doesn't work for me if I send the account as well... I think the message data is packed and sent over properly, while the account name is somehow passed through the permission_level I assume. I can remove the tuple packaging though as it doesn't do anything. Does it work for you with the suggested edit?
    – confused00
    Commented Jul 27, 2018 at 0:31
  • I tried a lot of different stuff, but I didn't try to reproduce your exact same code, so I'm not sure what's going on but it seems like maybe the receive action might not be actually handled by the receive method. According to my research, require_auth2(account, N(eosio.code)) should fail. Are you getting the console output from eosio::print(message)? Commented Jul 27, 2018 at 16:37
  • Yeah, works fine. The action is made to the receive method
    – confused00
    Commented Jul 27, 2018 at 16:41
  • This is it! Such an interesting solution. It seems that the eosio.code permission is not default to accounts, and not "virtual" as I've been reading. Seems you need to explicitly set the permission for the account.
    – Tyrick
    Commented Jul 27, 2018 at 17:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.