The revocable contract pattern
Intent
Allowing the creator of the smart contract to revoke functionality of certain methods.
Consequences
- The methods on which this pattern is applied are no longer functional once the smart contract is shut down.
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
- The oracle smart contract.
- The escrow to vote smart contract.
References
[1] Agoric, Zoe contract facet