How to add consumers to a Chainlink VRF2 subscription with an external smart contract

chainlinkchainlink-vrfcontract-invocationsolidity

When I try to add a consumer to my VRF2 subscription via the Subscription Manager, the transaction completes successfully. This also works when interacting with the chainlink contract directly on etherscan.

I'd like to deploy a contract that automatically adds itself to my existing chainlink subscription upon deployment. I have used the code provided by chainlink here.

The following code works on deployment:

    constructor() VRFConsumerBaseV2(vrfCoordinator) {
        COORDINATOR = VRFCoordinatorV2Interface(vrfCoordinator);
        LINKTOKEN = LinkTokenInterface(link_token_contract);
        s_owner = msg.sender;
        //Create a new subscription when you deploy the contract.
        createNewSubscription();
    }
      
    function createNewSubscription() private onlyOwner {
        s_subscriptionId = COORDINATOR.createSubscription();
        COORDINATOR.addConsumer(s_subscriptionId, address(this));
    }

As seen above the contract deploys and creates a new subscription and adds a consumer.
I originally simply wanted to add a consumer to an existing subscription so I modified the createNewSubscription function.

    // Create a new subscription when the contract is initially deployed.
    function createNewSubscription() private onlyOwner {
        s_subscriptionId = COORDINATOR.createSubscription();
        COORDINATOR.addConsumer(1234, address(this));
    }

This does not work. Of course I already have a subscription(1234) that works and can add to it manually, calling from another contract does not work. I have also tried assigning s_subscriptionId initially:
uint64 s_subscriptionId = 1234; which also does not seem to be working.

Further tests with (,s_subscriptionId,,) = COORDINATOR.getSubscription(); also failed.
Pointers on how to debug this would also be welcome as I'm learning solidity.

Best Answer

I have found the issue. Looking at VRFCoordinatorV2.sol The function addConsumer has a modifier called onlySubOwner here. Which is not explicitly outlined in the docs.

function addConsumer(uint64 subId, address consumer) external override onlySubOwner(subId) nonReentrant {
    // Already maxed, cannot add any more consumers.
    if (s_subscriptionConfigs[subId].consumers.length == MAX_CONSUMERS) {
      revert TooManyConsumers();
    }
    if (s_consumers[consumer][subId] != 0) {
      // Idempotence - do nothing if already added.
      // Ensures uniqueness in s_subscriptions[subId].consumers.
      return;
    }
    // Initialize the nonce to 1, indicating the consumer is allocated.
    s_consumers[consumer][subId] = 1;
    s_subscriptionConfigs[subId].consumers.push(consumer);

    emit SubscriptionConsumerAdded(subId, consumer);
  }
modifier onlySubOwner(uint64 subId) {
    address owner = s_subscriptionConfigs[subId].owner;
    if (owner == address(0)) {
      revert InvalidSubscription();
    }
    if (msg.sender != owner) {
      revert MustBeSubOwner(owner);
    }
    _;
  }

This means it is not possible to add a consumer if you're not the owner of the subscription.

I was trying to add the subscription via the deployed smart contract with delegate call, but it only accepts direct interactions from the subscription owner (which was my EOA on Metamask). It seems to be a safety feature rather than a bug, the way to go would be to use topUpSubscription to add the LINK token to the subscription programmatically or use the direct funding method.

Including an acceptSubscriptionOwnerTransfer in the deployed contract would be helpful for troubleshooting with an EOA in case of issues.

Related Topic