From 58f942f2bed2cb84e1b38c41a8c8896c979662db Mon Sep 17 00:00:00 2001 From: wighawag Date: Mon, 4 Sep 2023 19:02:10 +0100 Subject: [PATCH] docs --- contracts/docs_templates/{{contracts}}.hbs | 5 +- contracts/src/game/interface/IStratagems.sol | 20 +- .../game/interface/UsingStratagemsErrors.sol | 30 +++ .../game/interface/UsingStratagemsEvents.sol | 6 +- .../game/interface/UsingStratagemsTypes.sol | 4 + .../game/internal/UsingStratagemsState.sol | 2 +- docs/contracts/Gems.md | 7 + docs/contracts/Stratagems.md | 176 +++++++++++++++--- docs/contracts/TestTokens.md | 7 + package.json | 5 +- 10 files changed, 221 insertions(+), 41 deletions(-) diff --git a/contracts/docs_templates/{{contracts}}.hbs b/contracts/docs_templates/{{contracts}}.hbs index 91f2c9df..7ce8f9a6 100644 --- a/contracts/docs_templates/{{contracts}}.hbs +++ b/contracts/docs_templates/{{contracts}}.hbs @@ -66,7 +66,10 @@ outline: deep {{#each errors}} ### **{{{name}}}** -{{{notice}}} +{{#each notice}} +{{{this}}} + +{{/each}} {{{fullFormat}}} diff --git a/contracts/src/game/interface/IStratagems.sol b/contracts/src/game/interface/IStratagems.sol index 56a7955f..81daae99 100644 --- a/contracts/src/game/interface/IStratagems.sol +++ b/contracts/src/game/interface/IStratagems.sol @@ -36,7 +36,10 @@ interface IStratagemsGameplay is UsingStratagemsTypes, UsingStratagemsEvents { function addToReserve(uint256 tokensAmountToAdd, Permit calldata permit) external; /// @notice called by players to commit their moves - /// this can be called multiple time, the last call overriding the previous. + /// this can be called multiple time in the same epoch, the last call overriding the previous. + /// When a commitment is made, it needs to be resolved in the resolution phase of the same epoch.abi + /// If missed, player can still reveal its moves but none of them will be resolved. + /// The player would lose its associated reserved amount. /// @param commitmentHash the hash of the moves function makeCommitment(bytes24 commitmentHash) external; @@ -70,8 +73,10 @@ interface IStratagemsGameplay is UsingStratagemsTypes, UsingStratagemsEvents { /// @param secret the secret used to make the commit /// @param moves the actual moves /// @param furtherMoves if moves cannot be contained in one tx, further moves are represented by a hash to resolve too - /// Note that you have to that number of mvoes - /// @param useReserve whether the tokens are taken from the reserve or from approvals + /// Note that you have to that have enough moves (specified by MAX_NUM_MOVES_PER_HASH = 32) + /// @param useReserve whether the tokens are taken from the reserve or from approvals. + /// This allow player to keep their reserve intact and use it on their next move. + /// Note that this require the Stratagems contract to have enough allowance. function resolve( address player, bytes32 secret, @@ -80,7 +85,7 @@ interface IStratagemsGameplay is UsingStratagemsTypes, UsingStratagemsEvents { bool useReserve ) external; - /// @notice called by player if they missed the resolution phase and want to minimze the token loss + /// @notice called by player if they missed the resolution phase and want to minimze the token loss. /// By providing the moves, they will be slashed only the amount of token required to make the moves /// @param player the account who committed the move /// @param secret the secret used to make the commit @@ -95,14 +100,15 @@ interface IStratagemsGameplay is UsingStratagemsTypes, UsingStratagemsEvents { /// @notice should only be called as last resort /// this will burn all tokens in reserve - /// If player has access to the secret, better call acknowledgeMissedResolution + /// If player has access to the secret, better call `acknowledgeMissedResolution` function acknowledgeMissedResolutionByBurningAllReserve() external; - /// @notice poke a position, resolving its virtual state and if dead, reward neighboor enemies colors + /// @notice poke a position, resolving its virtual state. + // If dead as a result, it will reward neighboor enemies colors /// @param position the cell position function poke(uint64 position) external; - /// poke and collect the tokens won + /// @notice poke and collect the tokens won across multiple cells /// @param positions cell positions to collect from function pokeMultiple(uint64[] calldata positions) external; } diff --git a/contracts/src/game/interface/UsingStratagemsErrors.sol b/contracts/src/game/interface/UsingStratagemsErrors.sol index 1d1ca16a..4684a43a 100644 --- a/contracts/src/game/interface/UsingStratagemsErrors.sol +++ b/contracts/src/game/interface/UsingStratagemsErrors.sol @@ -2,14 +2,44 @@ pragma solidity ^0.8.0; interface UsingStratagemsErrors { + /// @notice Game has not started yet, can't perform any action error GameNotStarted(); + + /// @notice When in Resolution phase, it is not possible to commit new moves or cancel previous commitment + /// During Resolution phase, players have to reveal their commitment, if not already done. error InResolutionPhase(); + + /// @notice When in Commit phase, player can make new commitment but they cannot resolve their move yet. error InCommitmentPhase(); + + /// @notice Previous commitment need to be resolved before making a new one. Even if the corresponding reveal phase has passed.\ + /// It is also not possible to withdraw any amount from reserve until the commitment is revealed.\ + /// @notice If player lost the information to reveal, it can acknowledge failure which will burn all its reserve.\ error PreviousCommitmentNotResolved(); + + /// @notice to make a commitment you always need at least one `config.numTokensPerGems` amount in reserve + /// Player also need one `config.numTokensPerGems` per moves during the resolution phase. + /// @param inReserve amount in reserver as the time of the call + /// @param expected amount required to proceed error ReserveTooLow(uint256 inReserve, uint256 expected); + + /// @notice Player have to reveal their commitment using the exact same move values + /// If they provide different value, the commitment hash will differ and Stratagems will reject their resolution. error CommitmentHashNotMatching(); + + /// @notice Player can only resolve moves they commited. error NothingToResolve(); + + /// @notice Player can only resolve their move in the same epoch they commited.abi + /// If a player resolve later it can only do to minimize the reserve burn cost by calling : `acknowledgeMissedResolution` error InvalidEpoch(); + + /// @notice Player can make arbitrary number of moves per epoch. To do so they group moves into (MAX_NUM_MOVES_PER_HASH = 32) moves + /// This result in a recursive series of hash that they can then submit in turn while resolving. + /// The limit (MAX_NUM_MOVES_PER_HASH = 32) ensure a resolution batch fits in a block. error InvalidFurtherMoves(); + + /// @notice Player have to resolve if they can + /// Stratagems will prevent them from acknowledging missed resolution if there is still time to resolve. error CanStillResolve(); } diff --git a/contracts/src/game/interface/UsingStratagemsEvents.sol b/contracts/src/game/interface/UsingStratagemsEvents.sol index bedb8c16..d26c9f30 100644 --- a/contracts/src/game/interface/UsingStratagemsEvents.sol +++ b/contracts/src/game/interface/UsingStratagemsEvents.sol @@ -53,6 +53,10 @@ interface UsingStratagemsEvents is UsingStratagemsTypes { // Event to make it easier to check what is happening // TODO get rid ? // -------------------------------------------------------------------------------------------- - + /// @notice A move has been resolved. + /// @param position cell at which the move take place + /// @param player account making the move + /// @param oldColor previous color of the cell + /// @param newColor color that takes over event MoveProcessed(uint64 indexed position, address indexed player, Color oldColor, Color newColor); } diff --git a/contracts/src/game/interface/UsingStratagemsTypes.sol b/contracts/src/game/interface/UsingStratagemsTypes.sol index dd78948b..4b2a18e7 100644 --- a/contracts/src/game/interface/UsingStratagemsTypes.sol +++ b/contracts/src/game/interface/UsingStratagemsTypes.sol @@ -21,11 +21,13 @@ interface UsingStratagemsTypes { Evil } + /// @notice Move struct that define position and color struct Move { uint64 position; // TODO make it bigger ? uint32 * uint32 is probably infinitely big enough Color color; } + /// @notice Permit struct to authorize EIP2612 ERC20 contracts struct Permit { uint256 value; uint256 deadline; @@ -34,6 +36,7 @@ interface UsingStratagemsTypes { bytes32 s; } + /// @notice Config struct to configure the game instance struct Config { IERC20WithIERC2612 tokens; address payable burnAddress; @@ -44,6 +47,7 @@ interface UsingStratagemsTypes { uint256 numTokensPerGems; } + /// @notice Cell struct representing the current state of a cell struct FullCell { address owner; uint24 lastEpochUpdate; diff --git a/contracts/src/game/internal/UsingStratagemsState.sol b/contracts/src/game/internal/UsingStratagemsState.sol index db274d9a..a5a32a30 100644 --- a/contracts/src/game/internal/UsingStratagemsState.sol +++ b/contracts/src/game/internal/UsingStratagemsState.sol @@ -93,7 +93,7 @@ abstract contract UsingStratagemsState is address payable internal immutable BURN_ADDRESS; /// @notice the number of moves a hash represent, after that players make use of furtherMoves - uint8 internal constant MAX_NUM_MOVES_PER_HASH = 16; + uint8 internal constant MAX_NUM_MOVES_PER_HASH = 32; /// @notice Create an instance of a Stratagems game /// @param config configuration options for the game diff --git a/docs/contracts/Gems.md b/docs/contracts/Gems.md index 38601540..fed6cbd5 100644 --- a/docs/contracts/Gems.md +++ b/docs/contracts/Gems.md @@ -264,6 +264,7 @@ event Transfer(address indexed from, address indexed to, uint256 value) The permit has expired + error DeadlineOver(uint256 currentTime, uint256 deadline) | Name | Description @@ -275,6 +276,7 @@ error DeadlineOver(uint256 currentTime, uint256 deadline) An invalid address is specified (for example: zero address) + error InvalidAddress(address addr) | Name | Description @@ -285,6 +287,7 @@ error InvalidAddress(address addr) The msg value do not match the expected value + error InvalidMsgValue(uint256 provided, uint256 expected) | Name | Description @@ -296,12 +299,14 @@ error InvalidMsgValue(uint256 provided, uint256 expected) The signature do not match the expected signer + error InvalidSignature() ### **InvalidTotalAmount** The total amount provided do not match the expected value + error InvalidTotalAmount(uint256 provided, uint256 expected) | Name | Description @@ -313,6 +318,7 @@ error InvalidTotalAmount(uint256 provided, uint256 expected) the amount requested exceed the allowance + error NotAuthorizedAllowance(uint256 currentAllowance, uint256 expected) | Name | Description @@ -324,6 +330,7 @@ error NotAuthorizedAllowance(uint256 currentAllowance, uint256 expected) the amount requested exceed the balance + error NotEnoughTokens(uint256 currentBalance, uint256 expected) | Name | Description diff --git a/docs/contracts/Stratagems.md b/docs/contracts/Stratagems.md index dd87f1ae..a09494d5 100644 --- a/docs/contracts/Stratagems.md +++ b/docs/contracts/Stratagems.md @@ -9,7 +9,7 @@ outline: deep ### **acknowledgeMissedResolution** -called by player if they missed the resolution phase and want to minimze the token loss By providing the moves, they will be slashed only the amount of token required to make the moves +called by player if they missed the resolution phase and want to minimze the token loss. By providing the moves, they will be slashed only the amount of token required to make the moves *sig hash*: `0x81ca54b9` @@ -26,7 +26,7 @@ function acknowledgeMissedResolution(address player, bytes32 secret, tuple(uint6 ### **acknowledgeMissedResolutionByBurningAllReserve** -should only be called as last resort this will burn all tokens in reserve If player has access to the secret, better call acknowledgeMissedResolution +should only be called as last resort this will burn all tokens in reserve If player has access to the secret, better call `acknowledgeMissedResolution` *sig hash*: `0x24a27240` @@ -67,7 +67,7 @@ return updated cell (based on current epoch) *Signature*: getCell(uint256) -function getCell(uint256 id) view returns (tuple(address owner, uint32 lastEpochUpdate, uint32 epochWhenTokenIsAdded, uint8 color, uint8 life, int8 delta, uint8 enemymask)) +function getCell(uint256 id) view returns (tuple(address owner, uint24 lastEpochUpdate, uint24 epochWhenTokenIsAdded, uint8 color, uint8 life, int8 delta, uint8 enemyMap, uint8 distribution)) | Name | Description | ---- | ----------- @@ -81,7 +81,7 @@ return the list of updated cells (based on current epoch) whose ids is given *Signature*: getCells(uint256[]) -function getCells(uint256[] ids) view returns (tuple(address owner, uint32 lastEpochUpdate, uint32 epochWhenTokenIsAdded, uint8 color, uint8 life, int8 delta, uint8 enemymask)[] cells) +function getCells(uint256[] ids) view returns (tuple(address owner, uint24 lastEpochUpdate, uint24 epochWhenTokenIsAdded, uint8 color, uint8 life, int8 delta, uint8 enemyMap, uint8 distribution)[] cells) | Name | Description | ---- | ----------- @@ -95,7 +95,7 @@ The commitment to be resolved. zeroed if no commitment need to be made. *Signature*: getCommitment(address) -function getCommitment(address account) view returns (tuple(bytes24 hash, uint32 epoch) commitment) +function getCommitment(address account) view returns (tuple(bytes24 hash, uint24 epoch) commitment) | Name | Description | ---- | ----------- @@ -127,7 +127,7 @@ function getTokensInReserve(address account) view returns (uint256 amount) ### **makeCommitment** -called by players to commit their moves this can be called multiple time, the last call overriding the previous. +called by players to commit their moves this can be called multiple time in the same epoch, the last call overriding the previous. When a commitment is made, it needs to be resolved in the resolution phase of the same epoch.abi If missed, player can still reveal its moves but none of them will be resolved. The player would lose its associated reserved amount. *sig hash*: `0xb3015773` @@ -155,23 +155,9 @@ function makeCommitmentWithExtraReserve(bytes24 commitmentHash, uint256 tokensAm | tokensAmountToAdd | amount of tokens to add to the reserve. the resulting total must be enough to cover the moves | permit | permit EIP2612, value = zero if not needed -### **poke** - -poke a position, resolving its virtual state and if dead, reward neighboor enemies colors - -*sig hash*: `0x4dd3ab23` - -*Signature*: poke(uint64) - -function poke(uint64 position) - -| Name | Description -| ---- | ----------- -| position | the cell position - ### **pokeMultiple** -poke and collect the tokens won +poke and collect the tokens won across multiple cells *sig hash*: `0x8b8fc3a1` @@ -198,8 +184,8 @@ function resolve(address player, bytes32 secret, tuple(uint64 position, uint8 co | player | the account who committed the move | secret | the secret used to make the commit | moves | the actual moves -| furtherMoves | if moves cannot be contained in one tx, further moves are represented by a hash to resolve too Note that you have to that number of mvoes -| useReserve | whether the tokens are taken from the reserve or from approvals +| furtherMoves | if moves cannot be contained in one tx, further moves are represented by a hash to resolve too Note that you have to that have enough moves (specified by MAX_NUM_MOVES_PER_HASH = 32) +| useReserve | whether the tokens are taken from the reserve or from approvals. This allow player to keep their reserve intact and use it on their next move. Note that this require the Stratagems contract to have enough allowance. ### **withdrawFromReserve** @@ -232,7 +218,7 @@ function approve(address operator, uint256 tokenID) ### **balanceOf** -balanceOf is not implemented +balanceOf is not implemented, keeping track of this add gas and we did not consider that worth it *sig hash*: `0x70a08231` @@ -279,6 +265,34 @@ A descriptive name for a collection of NFTs in this contract function name() pure returns (string) +### **ownerAndLastTransferBlockNumberList** + +Get the list of owner of a token and the blockNumber of its last transfer, useful to voting mechanism. + +*sig hash*: `0xf3945282` + +*Signature*: ownerAndLastTransferBlockNumberList(uint256[]) + +function ownerAndLastTransferBlockNumberList(uint256[] tokenIDs) view returns (tuple(address owner, uint256 lastTransferBlockNumber)[] ownersData) + +| Name | Description +| ---- | ----------- +| tokenIDs | The list of token ids to check. + +### **ownerAndLastTransferBlockNumberOf** + +Get the owner of a token and the blockNumber of the last transfer, useful to voting mechanism. + +*sig hash*: `0x48f3c51c` + +*Signature*: ownerAndLastTransferBlockNumberOf(uint256) + +function ownerAndLastTransferBlockNumberOf(uint256 tokenID) view returns (address owner, uint256 blockNumber) + +| Name | Description +| ---- | ----------- +| tokenID | The id of the token. + ### **ownerOf** Get the owner of a token. @@ -348,7 +362,7 @@ Query if a contract implements an interface *Signature*: supportsInterface(bytes4) -function supportsInterface(bytes4 interfaceID) view returns (bool) +function supportsInterface(bytes4 interfaceID) pure returns (bool) | Name | Description | ---- | ----------- @@ -401,7 +415,7 @@ function transferFrom(address from, address to, uint256 tokenID) A player has cancelled its current commitment (before it reached the resolution phase) -event CommitmentCancelled(address indexed player, uint32 indexed epoch) +event CommitmentCancelled(address indexed player, uint24 indexed epoch) | Name | Description | ---- | ----------- @@ -412,7 +426,7 @@ event CommitmentCancelled(address indexed player, uint32 indexed epoch) A player has commited to make a move and resolve it on the resolution phase -event CommitmentMade(address indexed player, uint32 indexed epoch, bytes24 commitmentHash) +event CommitmentMade(address indexed player, uint24 indexed epoch, bytes24 commitmentHash) | Name | Description | ---- | ----------- @@ -424,7 +438,7 @@ event CommitmentMade(address indexed player, uint32 indexed epoch, bytes24 commi Player has resolved its previous commitment -event CommitmentResolved(address indexed player, uint32 indexed epoch, bytes24 indexed commitmentHash, tuple(uint64 position, uint8 color)[] moves, bytes24 furtherMoves, uint256 newReserveAmount) +event CommitmentResolved(address indexed player, uint24 indexed epoch, bytes24 indexed commitmentHash, tuple(uint64 position, uint8 color)[] moves, bytes24 furtherMoves, uint256 newReserveAmount) | Name | Description | ---- | ----------- @@ -438,7 +452,7 @@ event CommitmentResolved(address indexed player, uint32 indexed epoch, bytes24 i A player has canceled a previous commitment by burning some tokens -event CommitmentVoid(address indexed player, uint32 indexed epoch, uint256 amountBurnt, bytes24 furtherMoves) +event CommitmentVoid(address indexed player, uint24 indexed epoch, uint256 amountBurnt, bytes24 furtherMoves) | Name | Description | ---- | ----------- @@ -447,6 +461,19 @@ event CommitmentVoid(address indexed player, uint32 indexed epoch, uint256 amoun | amountBurnt | amount of token to burn | furtherMoves | hash of further moves, unless bytes32(0) which indicate end. +### **MoveProcessed** + +A move has been resolved. + +event MoveProcessed(uint64 indexed position, address indexed player, uint8 oldColor, uint8 newColor) + +| Name | Description +| ---- | ----------- +| position | cell at which the move take place +| player | account making the move +| oldColor | previous color of the cell +| newColor | color that takes over + ### **ReserveDeposited** Player has deposited token in the reserve, allowing it to use that much in game @@ -510,10 +537,90 @@ event Transfer(address indexed from, address indexed to, uint256 indexed tokenID ## Errors +### **CanStillResolve** + +Player have to resolve if they can Stratagems will prevent them from acknowledging missed resolution if there is still time to resolve. + + +error CanStillResolve() + +### **CommitmentHashNotMatching** + +Player have to reveal their commitment using the exact same move values If they provide different value, the commitment hash will differ and Stratagems will reject their resolution. + + +error CommitmentHashNotMatching() + +### **GameNotStarted** + +Game has not started yet, can't perform any action + + +error GameNotStarted() + +### **InCommitmentPhase** + +When in Commit phase, player can make new commitment but they cannot resolve their move yet. + + +error InCommitmentPhase() + +### **InResolutionPhase** + +When in Resolution phase, it is not possible to commit new moves or cancel previous commitment During Resolution phase, players have to reveal their commitment, if not already done. + + +error InResolutionPhase() + +### **InvalidEpoch** + +Player can only resolve their move in the same epoch they commited.abi If a player resolve later it can only do to minimize the reserve burn cost by calling : `acknowledgeMissedResolution` + + +error InvalidEpoch() + +### **InvalidFurtherMoves** + +Player can make arbitrary number of moves per epoch. To do so they group moves into (MAX_NUM_MOVES_PER_HASH = 32) moves This result in a recursive series of hash that they can then submit in turn while resolving. The limit (MAX_NUM_MOVES_PER_HASH = 32) ensure a resolution batch fits in a block. + + +error InvalidFurtherMoves() + +### **NothingToResolve** + +Player can only resolve moves they commited. + + +error NothingToResolve() + +### **PreviousCommitmentNotResolved** + +Previous commitment need to be resolved before making a new one. Even if the corresponding reveal phase has passed. + + It is also not possible to withdraw any amount from reserve until the commitment is revealed. + +If player lost the information to reveal, it can acknowledge failure which will burn all its reserve. + + +error PreviousCommitmentNotResolved() + +### **ReserveTooLow** + +to make a commitment you always need at least one `config.numTokensPerGems` amount in reserve Player also need one `config.numTokensPerGems` per moves during the resolution phase. + + +error ReserveTooLow(uint256 inReserve, uint256 expected) + +| Name | Description +| ---- | ----------- +| inReserve | amount in reserver as the time of the call +| expected | amount required to proceed + ### **InvalidAddress** An invalid address is specified (for example: zero address) + error InvalidAddress(address addr) | Name | Description @@ -524,6 +631,7 @@ error InvalidAddress(address addr) The token does not exist + error NonExistentToken(uint256 tokenID) | Name | Description @@ -534,12 +642,21 @@ error NonExistentToken(uint256 tokenID) The Nonce overflowed, make a transfer to self to allow new nonces. + error NonceOverflow() +### **NotAuthorized** + +Not authorized to perform this operation + + +error NotAuthorized() + ### **NotOwner** The address from which the token is sent is not the current owner + error NotOwner(address provided, address currentOwner) | Name | Description @@ -551,5 +668,6 @@ error NotOwner(address provided, address currentOwner) The Transfer was rejected by the destination + error TransferRejected() diff --git a/docs/contracts/TestTokens.md b/docs/contracts/TestTokens.md index d43275ed..e49561d1 100644 --- a/docs/contracts/TestTokens.md +++ b/docs/contracts/TestTokens.md @@ -261,6 +261,7 @@ event Transfer(address indexed from, address indexed to, uint256 value) The permit has expired + error DeadlineOver(uint256 currentTime, uint256 deadline) | Name | Description @@ -272,6 +273,7 @@ error DeadlineOver(uint256 currentTime, uint256 deadline) An invalid address is specified (for example: zero address) + error InvalidAddress(address addr) | Name | Description @@ -282,6 +284,7 @@ error InvalidAddress(address addr) The msg value do not match the expected value + error InvalidMsgValue(uint256 provided, uint256 expected) | Name | Description @@ -293,12 +296,14 @@ error InvalidMsgValue(uint256 provided, uint256 expected) The signature do not match the expected signer + error InvalidSignature() ### **InvalidTotalAmount** The total amount provided do not match the expected value + error InvalidTotalAmount(uint256 provided, uint256 expected) | Name | Description @@ -310,6 +315,7 @@ error InvalidTotalAmount(uint256 provided, uint256 expected) the amount requested exceed the allowance + error NotAuthorizedAllowance(uint256 currentAllowance, uint256 expected) | Name | Description @@ -321,6 +327,7 @@ error NotAuthorizedAllowance(uint256 currentAllowance, uint256 expected) the amount requested exceed the balance + error NotEnoughTokens(uint256 currentBalance, uint256 expected) | Name | Description diff --git a/package.json b/package.json index 8a04aa3e..33c26396 100644 --- a/package.json +++ b/package.json @@ -30,9 +30,10 @@ "indexer:dev": "pnpm --filter ./indexer dev", "common:dev": "pnpm --filter ./common dev", "contracts:test": "pnpm --filter ./contracts test", + "contracts:docs": "ldenv -m localhost pnpm run --filter ./contracts docgen @@MODE -o ../docs/contracts", "---------------------- DOCS ----------------------": "", - "docs:dev": "vitepress dev docs", - "docs:build": "vitepress build docs", + "docs:dev": "pnpm contracts:docs && vitepress dev docs", + "docs:build": "pnpm contracts:docs && vitepress build docs", "docs:preview": "vitepress preview docs", "---------------------- WEB USING EXISTING DEPLOYMENT ----------------------": "", "zellij-attach": "zellij --layout dev/zellij-attach.kdl a ${npm_package_name}-attach-$MODE || zellij --layout dev/zellij-attach.kdl -s ${npm_package_name}-attach-$MODE",