Een degelijke implementatie van het State Machine Design Pattern

Slimme contracten schrijven is eng. Ze verwerken echt geld en vergeten een enkel trefwoord toe te voegen, of 2 schijnbaar verwisselbare regels code verkeerd te ordenen, kan leiden tot het verlies van miljoenen dollars. Ja, u kunt tests schrijven en professionals uw code laten controleren, maar als de structuur en functionaliteit van uw contracten ingewikkeld worden, is de kans groot dat ze iets missen. U kunt niet 100% zeker zijn dat uw code veilig is.

Gelukkig bestaat er een overvloed aan best practices, bekende beveiligingsfouten en ontwerppatronen die u kunnen helpen de risico's te minimaliseren. Bij Token Foundry is een van de manieren waarop we onze tokenverkopen helpen beveiligen, gebruik van een patroon voor programmeerontwerp dat bekend staat als het "State Machine" -patroon.

Als je al een tijdje programmeert, ben je je misschien al bewust van dit patroon. Het doel van dit artikel is om uit te leggen:

  • Wat het State Machine-patroon is en wanneer het moet worden gebruikt
  • Een uitsplitsing van de staatsmachine die we bij Token Foundry hebben ontwikkeld, hoe u deze zelf kunt gebruiken en hoe deze eigenlijk werkt
  • Hoe dit patroon kan helpen om de veiligheid van uw contracten te waarborgen
  • Hoe we onze staatsmachine in de toekomst verder willen ontwikkelen

Wat is het State Machine Design Pattern?

Het State Machine-patroon splitst de functionaliteit van een programma op in een aantal verschillende "toestanden". Op een gegeven moment bevindt het programma zich in één en slechts één status, waarbij alleen statusspecifieke functionaliteit mogelijk is. Het programma kan op een vooraf gedefinieerde manier tussen deze toestanden overschakelen. Een programma kan bijvoorbeeld vereisen dat overgangen handmatig worden geactiveerd of kan automatisch schakelen tussen toestanden. Bij Token Foundry definiëren we deze automatische overgangen met behulp van startstatusvoorwaarden, waaronder (maar niet beperkt tot):

  • Een variabele heeft nu een gewenste waarde
  • Een specifiek tijdstip is bereikt
  • Er is een vereiste gebeurtenis- of functieaanroep opgetreden

Bij het invoeren van de nieuwe status kunnen wijzigingen in de variabele waarde optreden of kan bepaalde functionaliteit automatisch worden uitgevoerd. De functies die moeten worden uitgevoerd bij het invoeren van een nieuwe status zijn callback-functies.

Wanneer moet het patroon worden gebruikt?

Het ontwerppatroon van State Machine is niet geschikt voor gebruik in alle programma's of slimme contracten. Systemen die goed geschikt zijn voor het patroon moeten gemakkelijk worden onderverdeeld in verschillende fasen, waar verschillend gedrag optreedt of verschillende functionaliteit is toegestaan. Deze fasen van het systeem worden vertegenwoordigd door toestanden in een toestandsmachine en zouden na een bepaalde periode na verloop van tijd moeten plaatsvinden.

Wanneer bijvoorbeeld informatie on-chain wordt vrijgegeven, is het gebruikelijk dat alle partijen de hash van hun informatie plegen voordat ze de werkelijke waarden onthullen. Een voorbeeld hiervan is stemmen - het contract kan als volgt nodig hebben:

  • Registratie - kiezers kunnen zich bij het contract registreren om later te stemmen
  • Betrokkenheid van stemmen - hashes van de gekozen opties van de kiezer zijn toegewijd
  • Onthulling van stemmen - kiezers onthullen nu hun stem (die overeenkomt met hun hash)
  • Het stemmen is voorbij - geen inbreng van kiezers is toegestaan

De startvoorwaarden voor deze staten kunnen overgangen veroorzaken als een bepaald aantal kiezers zich heeft geregistreerd of als een bepaalde tijd is verstreken.

Een overzicht van de staatsmachine van Token Foundry en hoe u deze zelf kunt gebruiken

Bij Token Foundry hebben we enkele slimme contracten gecreëerd waarmee ontwikkelaars eenvoudig een lineair (voorlopig) machinepatroon kunnen implementeren. Onze contracten en tests met StateMachine zijn open-source en kunnen worden gevonden op de Token Foundry GitHub zodat iedereen deze kan lezen, testen en gebruiken.

Onze implementatie maakt het mogelijk om een ​​willekeurig aantal staten te definiëren, samen met een willekeurig aantal startvoorwaarden en callback-functies voor elke staat.

We bieden twee contracten: StateMachine.sol en TimedStateMachine.sol. De eerste hiervan is de implementatie van het basispatroon en de tweede is een uitbreiding die op tijdstempel gebaseerde startvoorwaarden voor toestanden mogelijk maakt.

Het basisidee voor het instellen van uw staatsmachine kan worden opgesplitst in een paar eenvoudige stappen:

  1. Identificeer de statussen op hoog niveau die uw machine zal hebben.

Ze worden gedefinieerd als constante waarden in het contract. In een eenvoudige tokenverkoop zou u bijvoorbeeld het volgende kunnen hebben:

bytes32 constant FREEZE = "bevriezen";
bytes32 constant IN_PROGRESS = "inProgress";
bytes32 constante ENDED = "afgelopen"

Deze statussen moeten worden doorgegeven aan de functie setStates (bytes32 [] statussen) om de statusmachine in te stellen. Dit moet meestal worden gedaan in de aannemer van uw contract.

2. Bepaal welke functies in elke staat worden toegestaan.

Dit wordt ook aanbevolen om te worden uitgevoerd in de aannemer van het contract, zodat alle niet-toegestane functies vanaf het begin als zodanig worden ingesteld. Als we bijvoorbeeld het bovenstaande voorbeeld voortzetten, willen we alleen dat een bijdrager tokens kan kopen tijdens onze IN_PROGRESS-status.

In de constructor zetten we:
allowFunction (IN_PROGRESS, this.buy.selector);

Dit stelt functie kopen in als alleen toegestaan ​​binnen IN_PROGRESS - als de statusmachine in FREEZE of ENDED staat, kan kopen niet worden uitgevoerd. Meer over hoe dit later werkt.

3. Definieer de startvoorwaarden en callback-functies van elke staat

Startconditie en callback-functies moeten worden gedefinieerd binnen uw smart contract en moeten worden toegevoegd aan de relevante statussen wanneer uw statusmachine wordt gebouwd.

Startvoorwaarden moeten de volgende vorm hebben, waarbij bytes32 de status-ID is (bijv. Constant FREEZE):
function exampleStartCondition (bytes32) interne retouren (bool) {...}
De callbacks die automatisch worden uitgevoerd bij het invoeren van een status, hebben een andere vorm:
function exampleCallback () internal {...}

Deze worden vervolgens als volgt ingesteld voor de relevante statussen:

addStartCondition (ENDED, hasSaleSoldOut);
addCallback (ENDED, transferMoneyToTeam);

Om door tijdstempels geactiveerde overgangen eenvoudiger te maken, hebben we ook een TimedStateMachine gedefinieerd. Dit contract heeft een door de tijdstempel geactiveerde startvoorwaarde en gebruikt de volgende functie om deze overgangen eenvoudig aan een statusmachine toe te voegen:
functie setStateStartTime (bytes32 stateId, uint256 timestamp) intern

Dus hoe werkt dit allemaal?

In ons StateMachine-contract hebben we een modificator genaamd checkAllowed. Die is als volgt gedefinieerd:

modifier checkAllowed {
    conditionalTransitions ();
    vereisen (staten [currentStateId] .allowedFunctions [msg.sig]);
    _;
}

Wanneer een functie wordt gedefinieerd met de modificatie checkAllowed, wordt eerst de functie conditionalTransitions () uitgevoerd. conditionalTransitions () controleert elk van de startvoorwaarden van de volgende status, en of een van deze echte overgangen naar die status zijn. Dit proces wordt herhaald totdat de statusmachine zich in de juiste huidige status bevindt. De volgende regel vereist dan dat de functie in de huidige status mag worden uitgevoerd.

Laten we bijvoorbeeld zeggen dat we een statusmachine hebben in status A en dat status B een startvoorwaarde heeft van tijd> = 22.00 uur. Als een functie alleenInStateA gemarkeerd checkAllowed wordt aangeroepen om 10.05pm, zal conditionalTransitions zien dat (time> = 10pm) == true, en de machine automatisch in status B zetten - op dit moment de nodige callbacks aanroepen. Het ziet dan dat de machine zich in staat B bevindt en niet alleenInStateA uitvoert.

Hoe onze staatsmachine helpt bij het verbeteren van codebeveiliging, redeneren en beheerbaarheid

Het gebruik van ontwerppatronen bij het programmeren - inclusief het statusmachinepatroon - splitst complexe ideeën op in eenvoudiger begrepen constructies. Met een systeem dat opnieuw is ontworpen als een toestandsmachine, kunnen toestanden afzonderlijk worden getest en beredeneerd, zonder het risico van interferentie van andere toestanden en hun gedrag.

Met State Machine-constructies kan ook de stroomhelderheid van een systeem worden verhoogd. Dergelijke eenvoud en duidelijkheid in slimme contracten is van cruciaal belang, vooral wanneer ze mogelijk grote sommen geld verwerken.

Beperkingen en toekomstplannen

Momenteel staat onze implementatie alleen lineaire machines toe (elke staat kan slechts 1 uitgaande overgang hebben). Dit werkt goed voor onze verkoopcontracten, die een eenvoudige stroom hebben en alleen bedoeld zijn om voor een korte periode te leven. Als uw systeem echter complexere functies vereist, is deze implementatie mogelijk niet voldoende. Voorbeelden van dergelijke kenmerken zijn het omkeren van een overgang, vertakkende stromen of het hebben van cycli.

We werken momenteel aan een refactoring van dit project om niet-lineaire machinestructuren en cycli te ondersteunen, in (hopelijk) de niet al te verre toekomst.

We waarderen alle vragen, vragen en feedback over de code die we schrijven, dus neem gerust contact op.

Alice Henshaw
Solidity Engineer @ Token Foundry
www.tokenfoundry.com