@@ -269,7 +269,7 @@ abstract contract GovernorUpgradeable is Initializable, ContextUpgradeable, ERC1
269269 }
270270
271271 /**
272- * @dev See {IGovernor-propose}.
272+ * @dev See {IGovernor-propose}. This function has opt-in frontrunning protection, described in {_isValidDescriptionForProposer}.
273273 */
274274 function propose (
275275 address [] memory targets ,
@@ -278,8 +278,9 @@ abstract contract GovernorUpgradeable is Initializable, ContextUpgradeable, ERC1
278278 string memory description
279279 ) public virtual override returns (uint256 ) {
280280 address proposer = _msgSender ();
281- uint256 currentTimepoint = clock ( );
281+ require ( _isValidDescriptionForProposer (proposer, description), " Governor: proposer restricted " );
282282
283+ uint256 currentTimepoint = clock ();
283284 require (
284285 getVotes (proposer, currentTimepoint - 1 ) >= proposalThreshold (),
285286 "Governor: proposer votes below proposal threshold "
@@ -641,6 +642,91 @@ abstract contract GovernorUpgradeable is Initializable, ContextUpgradeable, ERC1
641642 return this .onERC1155BatchReceived.selector ;
642643 }
643644
645+ /**
646+ * @dev Check if the proposer is authorized to submit a proposal with the given description.
647+ *
648+ * If the proposal description ends with `#proposer=0x???`, where `0x???` is an address written as a hex string
649+ * (case insensitive), then the submission of this proposal will only be authorized to said address.
650+ *
651+ * This is used for frontrunning protection. By adding this pattern at the end of their proposal, one can ensure
652+ * that no other address can submit the same proposal. An attacker would have to either remove or change that part,
653+ * which would result in a different proposal id.
654+ *
655+ * If the description does not match this pattern, it is unrestricted and anyone can submit it. This includes:
656+ * - If the `0x???` part is not a valid hex string.
657+ * - If the `0x???` part is a valid hex string, but does not contain exactly 40 hex digits.
658+ * - If it ends with the expected suffix followed by newlines or other whitespace.
659+ * - If it ends with some other similar suffix, e.g. `#other=abc`.
660+ * - If it does not end with any such suffix.
661+ */
662+ function _isValidDescriptionForProposer (
663+ address proposer ,
664+ string memory description
665+ ) internal view virtual returns (bool ) {
666+ uint256 len = bytes (description).length ;
667+
668+ // Length is too short to contain a valid proposer suffix
669+ if (len < 52 ) {
670+ return true ;
671+ }
672+
673+ // Extract what would be the `#proposer=0x` marker beginning the suffix
674+ bytes12 marker;
675+ assembly {
676+ // - Start of the string contents in memory = description + 32
677+ // - First character of the marker = len - 52
678+ // - Length of "#proposer=0x0000000000000000000000000000000000000000" = 52
679+ // - We read the memory word starting at the first character of the marker:
680+ // - (description + 32) + (len - 52) = description + (len - 20)
681+ // - Note: Solidity will ignore anything past the first 12 bytes
682+ marker := mload (add (description, sub (len, 20 )))
683+ }
684+
685+ // If the marker is not found, there is no proposer suffix to check
686+ if (marker != bytes12 ("#proposer=0x " )) {
687+ return true ;
688+ }
689+
690+ // Parse the 40 characters following the marker as uint160
691+ uint160 recovered = 0 ;
692+ for (uint256 i = len - 40 ; i < len; ++ i) {
693+ (bool isHex , uint8 value ) = _tryHexToUint (bytes (description)[i]);
694+ // If any of the characters is not a hex digit, ignore the suffix entirely
695+ if (! isHex) {
696+ return true ;
697+ }
698+ recovered = (recovered << 4 ) | value;
699+ }
700+
701+ return recovered == uint160 (proposer);
702+ }
703+
704+ /**
705+ * @dev Try to parse a character from a string as a hex value. Returns `(true, value)` if the char is in
706+ * `[0-9a-fA-F]` and `(false, 0)` otherwise. Value is guaranteed to be in the range `0 <= value < 16`
707+ */
708+ function _tryHexToUint (bytes1 char ) private pure returns (bool , uint8 ) {
709+ uint8 c = uint8 (char);
710+ unchecked {
711+ // Case 0-9
712+ if (47 < c && c < 58 ) {
713+ return (true , c - 48 );
714+ }
715+ // Case A-F
716+ else if (64 < c && c < 71 ) {
717+ return (true , c - 55 );
718+ }
719+ // Case a-f
720+ else if (96 < c && c < 103 ) {
721+ return (true , c - 87 );
722+ }
723+ // Else: not a hex char
724+ else {
725+ return (false , 0 );
726+ }
727+ }
728+ }
729+
644730 /**
645731 * @dev This empty reserved space is put in place to allow future versions to add new
646732 * variables without shifting down storage in the inheritance chain.
0 commit comments