Skip to the content.

The revocable contract pattern

Intent

Allowing the creator of the smart contract to revoke functionality of certain methods.

Consequences

Context

To shut down a smart contract, the zcf.shutdown() method should be called in the smart contract: this shuts down the entire vat and contract instance and gives payouts [1]. By issuing this shutdown method, all offer handlers directly become useless: if an invitation is issued in an offer after the zcf.shutdown() method has been called, then the Agoric SDK will return an error with message: ‘No further offers are accepted’. The zcf.shutdown() method however only disables the functionality of the offer handlers: all other methods are still operational. It is thus possible that an entity still holds a reference to a publicFacet of a smart contract that is already shut down, and that this entity can still use some of the methods provided by this publicFacet. It might be the case that the smart contract developer wants to revoke access to such methods when the smart contract is shut down. This can be achieved by checking whether a smart contract has already been revoked before executing the method: if the smart contract has not yet been revoked, then the method can execute, else the method must not execute. To implement this, the smart contract developer should do 3 things. First, the smart contract developer should add a boolean attribute revoked to the smart contract, which is initialized at false. Then, the smart contract developer should set revoked to true when the zcf.shutdown() method is called in the smart contract. Finally, the smart contract developer must add a check before each method that is not an offer handler and that should stop functioning when the smart contract is shut down. This check simply verifies whether the revoked attribute is equal to false, and will only execute the requested method if this is indeed the case.

Example

const start = async zcf => {
  const stop = seat => {
    zcf.shutdown('contract expired');
  };
  const printContactInfo = () => {
    return {
      name: "Alice",
      phoneNumber: "+32 some-phone-number",
      address: "Alice's address"
    }
  }
  const print = seat => {
    seat.exit();
    return {
      name: "Alice",
      phoneNumber: "+32 some-phone-number",
      address: "Alice's address"
    }
  }
  const creatorFacet = Far('creatorFacet', {
    makeStopInvitation: () => zcf.makeInvitation(stop, 'stop')
  });
  const publicFacet = Far('publicFacet', {
    getContactInfo: printContactInfo,
    getContactInfoInvite: () => zcf.makeInvitation(print, 'print')
  });
  return harden({ creatorFacet, publicFacet });
};
harden(start);
export { start };

The code above shows a smart contract that returns a creatorFacet and a publicFacet. Any entity that has a reference to the publicFacet can use the publicFacet to get the creator’s (here: Alice’s) contact information. This can be done in 2 ways: either directly via the printContactInfo, or by offering an invitation generated by the getContactInfoInvite. In the latter case, the seat that is returned once the getContactInfoInvite has been offered will get Alice’s contact information as the offer result. The creatorFacet can be used by Alice to shut down the smart contract. This way, other entities should no longer be able to get Alice’s contact information from the smart contract.

//Alice starts an instance of the installation
const { creatorFacet, publicFacet } = await zoe.startInstance(installation, {
  Asset: alphaCoin.issuer,
  Price: betaCoin.issuer,
});
//Alice shares the public facet with anyone that is interested
//Bob uses the publicFacet to get Alice's contact information
const alicesContactInfoForBob = await E(publicFacet).getContactInfo();
//check whether the contact info is as expected
t.deepEqual(
  alicesContactInfoForBob,
  {
    name: "Alice",
    phoneNumber: "+32 some-phone-number",
    address: "Alice's address"
  }
);
//Alice decides she no longer wants to advertise, and shuts down her smart contract
const stopInvitation = await E(creatorFacet).makeStopInvitation();
await E(zoe).offer(stopInvitation);
//Carol still has a reference to the publicFacet: she tries to get her info via an invitation
const alicesContactInfoForCarolInvitation = await E(publicFacet).getContactInfoInvite();
//since zcf.shutdown() is called, Agoric does not allow this
await t.throwsAsync(async () =>
{
  await E(zoe).offer(alicesContactInfoForCarolInvitation);
},
{
  instanceOf: Error,
  message: 'No further offers are accepted'
}
);
//Carol is still able to get Alice's contact info, even after Alice called shutdown
const alicesContactInfoForCarol = await E(publicFacet).getContactInfo();
t.deepEqual(
  alicesContactInfoForCarol,
  {
    name: "Alice",
    phoneNumber: "+32 some-phone-number",
    address: "Alice's address"
  }
);    

In the test above, Alice starts an instance of the initial smart contract. While the smart contract is active, entities can get Alice’s contact information via the publicFacet. At a given point in time, Alice decides to shut down the smart contract by offering the invitation that she got from the stop method on her creatorFacet. Carol, however, still has a reference to the publicFacet. Carol first tries to get Alice’s contact information by issuing the invitation that she got from calling the getContactInfoInvite method on the publicFacet. This fails: Zoe throws an error in which it states that offers are no longer accepted. Carol then attempts to use the getContactInfo method on the publicFacet to get Alice’s contact information. This still works, since the initial smart contract does not yet implement the revocable contract pattern.

const start = async zcf => {
  let revoked = false;
  const stop = seat => {
    revoked = true;
    zcf.shutdown('contract expired');
  };
  const printContactInfo = () => {
    if(!revoked) {
        return {
            name: "Alice",
            phoneNumber: "+32 some-phone-number",
            address: "Alice's address"
        }
    }
    return "This contract is no longer in use";
  }
  const print = seat => {
    seat.exit();
    return {
      name: "Alice",
      phoneNumber: "+32 some-phone-number",
      address: "Alice's address"
    }
  }
  const creatorFacet = Far('creatorFacet', {
    makeStopInvitation: () => zcf.makeInvitation(stop, 'stop')
  });
  const publicFacet = Far('publicFacet', {
    getContactInfo: printContactInfo,
    getContactInfoInvite: () => zcf.makeInvitation(print, 'print')
  });
  return harden({ creatorFacet, publicFacet });
};
harden(start);
export { start };

The code above shows the revocable contract pattern applied to the initial smart contract. This code shows that a revoked attribute is added to the smart contract. This is a boolean attribute that is initialized as false. When the stop method gets executed, the revoked attribute is set to true. This means that, as soon as the stop method is executed, the printContractInfo method provided by the publicFacet will no longer return Alice’s contract information. This is because a check is added to the printContractInfo method: the method will only return Alice’s contract info if the revoked attribute is set to false. Else, it will return a message stating that the smart contract has been shut down.

//Alice starts an instance of the installation
const { creatorFacet, publicFacet } = await zoe.startInstance(installation, {
  Asset: alphaCoin.issuer,
  Price: betaCoin.issuer,
});
//Alice shares the public facet with anyone that is interested
//Bob uses the publicFacet to get Alice's contact information
const alicesContactInfoForBob = await E(publicFacet).getContactInfo();
//check whether the contact info is as expected
t.deepEqual(
  alicesContactInfoForBob,
  {
    name: "Alice",
    phoneNumber: "+32 some-phone-number",
    address: "Alice's address"
  }
);
//Alice decides she no longer wants to advertise, and shuts down her smart contract
const stopInvitation = await E(creatorFacet).makeStopInvitation();
await E(zoe).offer(stopInvitation);
//Carol still has a reference to the publicFacet: she tries to get her info via an invitation
const alicesContactInfoForCarolInvitation = await E(publicFacet).getContactInfoInvite();
//since zcf.shutdown() is called, Agoric does not allow this
await t.throwsAsync(async () =>
  {
    await E(zoe).offer(alicesContactInfoForCarolInvitation);
  },
  {
    instanceOf: Error,
    message: 'No further offers are accepted'
  }
);
//Carol tries to get Alice's contact information via the publicFacet
//this no longer works due to the revocable contract pattern
const alicesContactInfoForCarol = await E(publicFacet).getContactInfo();
//Carol can see that this information is no longer available
t.deepEqual(alicesContactInfoForCarol, "This contract is no longer in use");

As shown in the tests above: due to the implementation of the revocable contract pattern, Carol is no longer able to get Alice’s contract information. Instead, Carol gets a message stating that the contract is no longer active.

General rule:

If a smart contract returns a publicFacet, and the publicFacet provides a non-payment related method that should be revoked after the smart contract has been shut down, then the revocable contract pattern should be used to shut down this functionality together with the smart contract.

Known uses

References

[1] Agoric, Zoe contract facet