Configuring PlatformGeneric FacetsState Facet

State Facets Initialization

Setting States and Selectors

When internal state facets are initialized with the respective uint256 values, you can begin initializing the StateFacetStorage. To simplify development, you can use an enum to define the states, with each state corresponding to its index position and represented as a uint256 value.

enum STATES {
    NON_EXISTENT, // 0
    FRACTIONS_CREATED, // 1
    PURCHASE, // 2
    RECEIVE, // 3
    DONE, // 4
    NON_FUNDED, // 5
    REJECTED, // 6
  } // NON_EXISTENT (default)

Note

State 0 is the default (nonexistent campaign) state, State 1 always follows a createFractions() call, and all subsequent states are configurable based on platform requirements.

After defining the states in the enum, you have to create a variable (_initStateArg) that would contain encoded data which will be passed to the function when initializing the StateFacetStorage.

Besides states, it includes before and after hooks - facet functions that get executed when the changeState() function is called.

For instance, when changeState(campaignId, from = X, to = Y) is called:

  • First, all afterHooks associated with state X (the departing state) are executed, if any.
  • Then, all beforeHooks associated with state Y (the arriving state) are executed, if any.

You must use selectors when filling out the before and after hook arrays. Below you can find a code example that highlights the creation of encoded data for the initialization of the StateFacetStorage. Consider this example solely for demonstration purposes, as it should be properly refined for your specific use case:

const _initStateArg = hre.ethers.utils.defaultAbiCoder.encode(
    ["uint256", "bytes4[][]", "bytes4[][]", "uint256[][]"],
    [
      hre.ethers.BigNumber.from("7"), // The platform has 7 distinct states (0,1,2,3,4,5,6)
      [["0x8f3c1a7b"], ["0x3b5afc80", "0xa11498f2"], [], [], [], [], []], // Selectors for before hooks in any state
      [["0xd2e47c19"], ["0xc9427b3d"], [], [], [], [], []], // Selectors for after hooks in any state
      [
        [STATES.FRACTIONS_CREATED], // 0 --> {1}
        [STATES.PURCHASE, STATES.REJECTED], // 1 --> {2, 6}
        [STATES.RECEIVE, STATES.NON_FUNDED], // 2 --> {3, 5}
        [STATES.DONE], // 3 --> {4}
        [], // 4 --> null
        [], // 5 --> null
        [], // 6 --> null
      ],
    ]
  );
  await stateFacet.initStateFacet(_initStateArg);

This 2D array represents a state transition matrix, where each row corresponds to a specific state, and the entries in that row indicate the states it can transition to. In this matrix X, the array at index 1 is [STATES.PURCHASE, STATES.REJECTED], indicating that from state 1 (FRACTIONS_CREATED), transitions to state 2 (PURCHASE) and state 6 (REJECTED) are allowed.

X[1][1] implies a valid transition from state 1 to state 6. Indices 4, 5, and 6 (corresponding to states DONE, NON_FUNDED, and REJECTED, respectively) contain empty arrays. This indicates that once a campaign reaches any of these terminal states, no further transitions are allowed—it remains in that state permanently.

The state and selector arrays can include multiple state options and function selectors, which are executed in sequence.

Each index in the state array aligns with the corresponding before and after hook arrays at the same position. For example:

["0x8f3c1a7b"]
["0xd2e47c19"]
[STATES.FRACTIONS_CREATED]

Note

To learn how to obtain function selectors for each required facet and initialize them, refer to the RWA Fraction Market guide.

The changeState() function in StateFacetStorage reveals an important detail: the after hooks are executed before the before hooks, which may seem counterintuitive at first glance:

function changeState(Layout storage l, uint256 campaignId, uint256 fromState, uint256 toState) internal {
    if (l.stateOfId[campaignId] != fromState) revert FromStateNotCurrent(fromState, l.stateOfId[campaignId]);
    if (toState >= l.totalStatesSupported) revert StateExceedsSupportedLimit(toState, l.totalStatesSupported);
 
    StateHooks storage stateHooksInfoFrom = l.stateHooksInfo[fromState];
    StateHooks storage stateHooksInfoTo = l.stateHooksInfo[toState];
 
    if (!stateHooksInfoFrom.isStateTransitionSupported[toState]) revert StateTransitionNotSupported(fromState, toState);
 
    execute(stateHooksInfoFrom.afterSelectors, campaignId);
    execute(stateHooksInfoTo.beforeSelectors, campaignId);
 
    l.stateOfId[campaignId] = toState;
}

The reason this happens is because when a campaign transitions from state x to state y, it first goes through the after hooks, marking the end of the current state x, and then, upon entering state y, it calls the before hooks of the new state:

image

On this page