diff --git a/contracts/access/access_control/_AccessControl.sol b/contracts/access/access_control/_AccessControl.sol index 947dcff6..76b79240 100644 --- a/contracts/access/access_control/_AccessControl.sol +++ b/contracts/access/access_control/_AccessControl.sol @@ -6,13 +6,14 @@ import { EnumerableSet } from '../../data/EnumerableSet.sol'; import { AccessControlStorage } from '../../storage/AccessControlStorage.sol'; import { Address } from '../../utils/Address.sol'; import { Uint256 } from '../../utils/Uint256.sol'; +import { _Context } from '../../meta/_Context.sol'; import { _IAccessControl } from './_IAccessControl.sol'; /** * @title Role-based access control system * @dev derived from https://github.com/OpenZeppelin/openzeppelin-contracts (MIT license) */ -abstract contract _AccessControl is _IAccessControl { +abstract contract _AccessControl is _IAccessControl, _Context { using Address for address; using EnumerableSet for EnumerableSet.AddressSet; using Uint256 for uint256; @@ -45,7 +46,7 @@ abstract contract _AccessControl is _IAccessControl { * @param role role to query */ function _checkRole(bytes32 role) internal view virtual { - _checkRole(role, msg.sender); + _checkRole(role, _msgSender()); } /** @@ -117,7 +118,7 @@ abstract contract _AccessControl is _IAccessControl { * @param role role to relinquish */ function _renounceRole(bytes32 role) internal virtual { - _setRole(role, msg.sender, false); + _setRole(role, _msgSender(), false); } function _setRole( @@ -131,14 +132,14 @@ abstract contract _AccessControl is _IAccessControl { .roles[role] .members .add(account); - emit RoleGranted(role, account, msg.sender); + emit RoleGranted(role, account, _msgSender()); } else { AccessControlStorage .layout(AccessControlStorage.DEFAULT_STORAGE_SLOT) .roles[role] .members .remove(account); - emit RoleRevoked(role, account, msg.sender); + emit RoleRevoked(role, account, _msgSender()); } } diff --git a/contracts/access/access_control/_IAccessControl.sol b/contracts/access/access_control/_IAccessControl.sol index c3fc5710..d3fce98f 100644 --- a/contracts/access/access_control/_IAccessControl.sol +++ b/contracts/access/access_control/_IAccessControl.sol @@ -2,10 +2,12 @@ pragma solidity ^0.8.24; +import { _IContext } from '../../meta/_IContext.sol'; + /** * @title Partial AccessControl interface needed by internal functions */ -interface _IAccessControl { +interface _IAccessControl is _IContext { event RoleAdminChanged( bytes32 indexed role, bytes32 indexed previousAdminRole, diff --git a/contracts/access/ownable/IOwnable.sol b/contracts/access/ownable/IOwnable.sol index a53a49da..836c81dd 100644 --- a/contracts/access/ownable/IOwnable.sol +++ b/contracts/access/ownable/IOwnable.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.24; import { IERC173 } from '../../interfaces/IERC173.sol'; +import { IContext } from '../../meta/IContext.sol'; import { _IOwnable } from './_IOwnable.sol'; -interface IOwnable is _IOwnable, IERC173 {} +interface IOwnable is _IOwnable, IContext, IERC173 {} diff --git a/contracts/access/ownable/Ownable.sol b/contracts/access/ownable/Ownable.sol index b19f8c0c..35e73618 100644 --- a/contracts/access/ownable/Ownable.sol +++ b/contracts/access/ownable/Ownable.sol @@ -3,13 +3,14 @@ pragma solidity ^0.8.24; import { IERC173 } from '../../interfaces/IERC173.sol'; +import { Context } from '../../meta/Context.sol'; import { IOwnable } from './IOwnable.sol'; import { _Ownable } from './_Ownable.sol'; /** * @title Ownership access control based on ERC173 */ -abstract contract Ownable is IOwnable, _Ownable { +abstract contract Ownable is IOwnable, _Ownable, Context { /** * @inheritdoc IERC173 */ diff --git a/contracts/access/ownable/_IOwnable.sol b/contracts/access/ownable/_IOwnable.sol index 74a5c011..1960e518 100644 --- a/contracts/access/ownable/_IOwnable.sol +++ b/contracts/access/ownable/_IOwnable.sol @@ -3,8 +3,9 @@ pragma solidity ^0.8.24; import { _IERC173 } from '../../interfaces/_IERC173.sol'; +import { _IContext } from '../../meta/_IContext.sol'; -interface _IOwnable is _IERC173 { +interface _IOwnable is _IERC173, _IContext { error Ownable__NotOwner(); error Ownable__NotTransitiveOwner(); } diff --git a/contracts/access/ownable/_Ownable.sol b/contracts/access/ownable/_Ownable.sol index 071720d9..23adc2bb 100644 --- a/contracts/access/ownable/_Ownable.sol +++ b/contracts/access/ownable/_Ownable.sol @@ -5,18 +5,19 @@ pragma solidity ^0.8.24; import { IERC173 } from '../../interfaces/IERC173.sol'; import { ERC173Storage } from '../../storage/ERC173Storage.sol'; import { Address } from '../../utils/Address.sol'; +import { _Context } from '../../meta/_Context.sol'; import { _IOwnable } from './_IOwnable.sol'; -abstract contract _Ownable is _IOwnable { +abstract contract _Ownable is _IOwnable, _Context { using Address for address; modifier onlyOwner() { - if (msg.sender != _owner()) revert Ownable__NotOwner(); + if (_msgSender() != _owner()) revert Ownable__NotOwner(); _; } modifier onlyTransitiveOwner() { - if (msg.sender != _transitiveOwner()) + if (_msgSender() != _transitiveOwner()) revert Ownable__NotTransitiveOwner(); _; } diff --git a/contracts/access/ownable/_SafeOwnable.sol b/contracts/access/ownable/_SafeOwnable.sol index 1f3cf55e..df89c583 100644 --- a/contracts/access/ownable/_SafeOwnable.sol +++ b/contracts/access/ownable/_SafeOwnable.sol @@ -8,7 +8,7 @@ import { _Ownable } from './_Ownable.sol'; abstract contract _SafeOwnable is _ISafeOwnable, _Ownable { modifier onlyNomineeOwner() { - if (msg.sender != _nomineeOwner()) + if (_msgSender() != _nomineeOwner()) revert SafeOwnable__NotNomineeOwner(); _; } @@ -27,7 +27,7 @@ abstract contract _SafeOwnable is _ISafeOwnable, _Ownable { * @notice accept transfer of contract ownership */ function _acceptOwnership() internal virtual onlyNomineeOwner { - _setOwner(msg.sender); + _setOwner(_msgSender()); delete ERC173Storage .layout(ERC173Storage.DEFAULT_STORAGE_SLOT) .nomineeOwner; diff --git a/contracts/interfaces/IERC2771.sol b/contracts/interfaces/IERC2771.sol new file mode 100644 index 00000000..52e65a3a --- /dev/null +++ b/contracts/interfaces/IERC2771.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +import { _IERC2771 } from './_IERC2771.sol'; + +/** + * @title ERC2771 interface + * @dev see https://eips.ethereum.org/EIPS/eip-2771 + */ +interface IERC2771 is _IERC2771 { + /** + * @notice query whether account is a trusted ERC2771 forwarder + * @param account address to query + * @return trustedStatus whether account is a trusted forwarder + */ + function isTrustedForwarder( + address account + ) external view returns (bool trustedStatus); +} diff --git a/contracts/interfaces/_IERC2771.sol b/contracts/interfaces/_IERC2771.sol new file mode 100644 index 00000000..de6532a9 --- /dev/null +++ b/contracts/interfaces/_IERC2771.sol @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +interface _IERC2771 {} diff --git a/contracts/meta/Context.sol b/contracts/meta/Context.sol new file mode 100644 index 00000000..131db583 --- /dev/null +++ b/contracts/meta/Context.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +import { IContext } from './IContext.sol'; +import { _Context } from './_Context.sol'; + +abstract contract Context is IContext, _Context {} diff --git a/contracts/meta/ForwardedMetaTransactionContext.sol b/contracts/meta/ForwardedMetaTransactionContext.sol new file mode 100644 index 00000000..3b830b00 --- /dev/null +++ b/contracts/meta/ForwardedMetaTransactionContext.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +import { IERC2771 } from '../interfaces/IERC2771.sol'; +import { Context } from './Context.sol'; +import { _Context } from './_Context.sol'; +import { IForwardedMetaTransactionContext } from './IForwardedMetaTransactionContext.sol'; +import { _ForwardedMetaTransactionContext } from './_ForwardedMetaTransactionContext.sol'; + +abstract contract ForwardedMetaTransactionContext is + IForwardedMetaTransactionContext, + _ForwardedMetaTransactionContext, + Context +{ + /** + * @inheritdoc IERC2771 + */ + function isTrustedForwarder( + address account + ) external view returns (bool trustedStatus) { + trustedStatus = _isTrustedForwarder(account); + } + + /** + * @inheritdoc _ForwardedMetaTransactionContext + */ + function _msgSender() + internal + view + virtual + override(_Context, _ForwardedMetaTransactionContext) + returns (address msgSender) + { + msgSender = super._msgSender(); + } + + /** + * @inheritdoc _ForwardedMetaTransactionContext + */ + function _msgData() + internal + view + virtual + override(_Context, _ForwardedMetaTransactionContext) + returns (bytes calldata msgData) + { + msgData = super._msgData(); + } + + /** + * @inheritdoc _ForwardedMetaTransactionContext + */ + function _calldataSuffixLength() + internal + view + virtual + override(_Context, _ForwardedMetaTransactionContext) + returns (uint256 length) + { + length = super._calldataSuffixLength(); + } +} diff --git a/contracts/meta/IContext.sol b/contracts/meta/IContext.sol new file mode 100644 index 00000000..b089eba4 --- /dev/null +++ b/contracts/meta/IContext.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +import { _IContext } from './_IContext.sol'; + +interface IContext is _IContext {} diff --git a/contracts/meta/IForwardedMetaTransactionContext.sol b/contracts/meta/IForwardedMetaTransactionContext.sol new file mode 100644 index 00000000..ca2dbbb5 --- /dev/null +++ b/contracts/meta/IForwardedMetaTransactionContext.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +import { IERC2771 } from '../interfaces/IERC2771.sol'; +import { IContext } from './IContext.sol'; +import { _IForwardedMetaTransactionContext } from './_IForwardedMetaTransactionContext.sol'; + +interface IForwardedMetaTransactionContext is + _IForwardedMetaTransactionContext, + IContext, + IERC2771 +{} diff --git a/contracts/meta/_Context.sol b/contracts/meta/_Context.sol new file mode 100644 index 00000000..e344ded6 --- /dev/null +++ b/contracts/meta/_Context.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +import { _IContext } from './_IContext.sol'; + +/** + * @title Utility contract for supporting alternative authorization schemes + */ +abstract contract _Context is _IContext { + /** + * @notice return the message sender + * @dev if no Context extension is in use, msg.sender is returned as-is + * @return msgSender account contextualized as message sender + */ + function _msgSender() internal view virtual returns (address msgSender) { + msgSender = msg.sender; + } + + /** + * @notice return the message data + * @dev if no Context extension is in use, msg.data is returned as-is + * @return msgData message data with suffix removed, if applicable + */ + function _msgData() internal view virtual returns (bytes calldata msgData) { + msgData = msg.data; + } + + /** + * @notice return the bytes length of the calldata context suffix + * @dev if no Context extension is in use, suffix length is 0 + * @return length length of calldata context suffix + */ + function _calldataSuffixLength() + internal + view + virtual + returns (uint256 length) + { + length = 0; + } +} diff --git a/contracts/meta/_ForwardedMetaTransactionContext.sol b/contracts/meta/_ForwardedMetaTransactionContext.sol new file mode 100644 index 00000000..409657f5 --- /dev/null +++ b/contracts/meta/_ForwardedMetaTransactionContext.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +import { ERC2771Storage } from '../storage/ERC2771Storage.sol'; +import { Address } from '../utils/Address.sol'; +import { _Context } from './_Context.sol'; +import { _IForwardedMetaTransactionContext } from './_IForwardedMetaTransactionContext.sol'; + +abstract contract _ForwardedMetaTransactionContext is + _IForwardedMetaTransactionContext, + _Context +{ + using Address for address; + + /** + * @inheritdoc _Context + * @dev sender is read from the calldata context suffix + */ + function _msgSender() + internal + view + virtual + override + returns (address msgSender) + { + uint256 dataLength = msg.data.length; + uint256 suffixLength = _calldataSuffixLength(); + + // ideally this would revert when dataLength < suffixLength and sender is a trusted forwarder + // but because _isTrustedForwarder reads from storage, it should be called only when necessary + + if (dataLength >= suffixLength && _isTrustedForwarder(msg.sender)) { + unchecked { + msgSender = address( + bytes20(msg.data[dataLength - suffixLength:]) + ); + } + } else { + msgSender = super._msgSender(); + } + } + + /** + * @inheritdoc _Context + */ + function _msgData() + internal + view + virtual + override + returns (bytes calldata msgData) + { + uint256 dataLength = msg.data.length; + uint256 suffixLength = _calldataSuffixLength(); + + // ideally this would revert when dataLength < suffixLength and sender is a trusted forwarder + // but because _isTrustedForwarder reads from storage, it should be called only when necessary + + if (dataLength >= suffixLength && _isTrustedForwarder(msg.sender)) { + unchecked { + msgData = msg.data[:dataLength - suffixLength]; + } + } else { + msgData = super._msgData(); + } + } + + /** + * @inheritdoc _Context + * @dev this Context extension defines an address suffix with a length of 20 + */ + function _calldataSuffixLength() + internal + view + virtual + override + returns (uint256 length) + { + length = 20; + } + + /** + * @notice query whether account is a trusted ERC2771 forwarder + * @param account address to query + * @return trustedStatus whether account is a trusted forwarder + */ + function _isTrustedForwarder( + address account + ) internal view virtual returns (bool trustedStatus) { + trustedStatus = ERC2771Storage + .layout(ERC2771Storage.DEFAULT_STORAGE_SLOT) + .trustedForwarders[account]; + } + + /** + * @notice grant trusted forwarder status to account + * @param account account whose trusted forwarder status to grant + */ + function _addTrustedForwarder(address account) internal virtual { + ERC2771Storage + .layout(ERC2771Storage.DEFAULT_STORAGE_SLOT) + .trustedForwarders[account] = true; + } + + /** + * @notice revoke trusted forwarder status from account + * @param account account whose trusted forwarder status to revoke + */ + function _removeTrustedForwarder(address account) internal virtual { + delete ERC2771Storage + .layout(ERC2771Storage.DEFAULT_STORAGE_SLOT) + .trustedForwarders[account]; + } +} diff --git a/contracts/meta/_IContext.sol b/contracts/meta/_IContext.sol new file mode 100644 index 00000000..08e10713 --- /dev/null +++ b/contracts/meta/_IContext.sol @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +interface _IContext {} diff --git a/contracts/meta/_IForwardedMetaTransactionContext.sol b/contracts/meta/_IForwardedMetaTransactionContext.sol new file mode 100644 index 00000000..d442deaa --- /dev/null +++ b/contracts/meta/_IForwardedMetaTransactionContext.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +import { _IERC2771 } from '../interfaces/_IERC2771.sol'; +import { _IContext } from './_IContext.sol'; + +interface _IForwardedMetaTransactionContext is _IContext, _IERC2771 {} diff --git a/contracts/proxy/diamond/SolidstateDiamondProxy.sol b/contracts/proxy/diamond/SolidstateDiamondProxy.sol index fa89f71b..fd418d6b 100644 --- a/contracts/proxy/diamond/SolidstateDiamondProxy.sol +++ b/contracts/proxy/diamond/SolidstateDiamondProxy.sol @@ -96,7 +96,7 @@ abstract contract SolidstateDiamondProxy is // set owner - _setOwner(msg.sender); + _setOwner(_msgSender()); } function _transferOwnership( diff --git a/contracts/security/partially_pausable/_IPartiallyPausable.sol b/contracts/security/partially_pausable/_IPartiallyPausable.sol index fc8f9053..7c7c9b36 100644 --- a/contracts/security/partially_pausable/_IPartiallyPausable.sol +++ b/contracts/security/partially_pausable/_IPartiallyPausable.sol @@ -2,7 +2,9 @@ pragma solidity ^0.8.24; -interface _IPartiallyPausable { +import { _IContext } from '../../meta/_IContext.sol'; + +interface _IPartiallyPausable is _IContext { error PartiallyPausable__PartiallyPaused(); error PartiallyPausable__NotPartiallyPaused(); diff --git a/contracts/security/partially_pausable/_PartiallyPausable.sol b/contracts/security/partially_pausable/_PartiallyPausable.sol index d878782b..d4dd36a1 100644 --- a/contracts/security/partially_pausable/_PartiallyPausable.sol +++ b/contracts/security/partially_pausable/_PartiallyPausable.sol @@ -3,12 +3,13 @@ pragma solidity ^0.8.24; import { PausableStorage } from '../../storage/PausableStorage.sol'; +import { _Context } from '../../meta/_Context.sol'; import { _IPartiallyPausable } from './_IPartiallyPausable.sol'; /** * @title Internal functions for PartiallyPausable security control module. */ -abstract contract _PartiallyPausable is _IPartiallyPausable { +abstract contract _PartiallyPausable is _IPartiallyPausable, _Context { modifier whenNotPartiallyPaused(bytes32 key) { if (_partiallyPaused(key)) revert PartiallyPausable__PartiallyPaused(); _; @@ -42,7 +43,7 @@ abstract contract _PartiallyPausable is _IPartiallyPausable { PausableStorage .layout(PausableStorage.DEFAULT_STORAGE_SLOT) .partiallyPaused[key] = true; - emit PartiallyPaused(msg.sender, key); + emit PartiallyPaused(_msgSender(), key); } /** @@ -55,6 +56,6 @@ abstract contract _PartiallyPausable is _IPartiallyPausable { delete PausableStorage .layout(PausableStorage.DEFAULT_STORAGE_SLOT) .partiallyPaused[key]; - emit PartiallyUnpaused(msg.sender, key); + emit PartiallyUnpaused(_msgSender(), key); } } diff --git a/contracts/security/pausable/IPausable.sol b/contracts/security/pausable/IPausable.sol index fa181c40..75ed9ada 100644 --- a/contracts/security/pausable/IPausable.sol +++ b/contracts/security/pausable/IPausable.sol @@ -2,9 +2,10 @@ pragma solidity ^0.8.24; +import { IContext } from '../../meta/IContext.sol'; import { _IPausable } from './_IPausable.sol'; -interface IPausable is _IPausable { +interface IPausable is _IPausable, IContext { /** * @notice query whether contract is paused * @return status whether contract is paused diff --git a/contracts/security/pausable/Pausable.sol b/contracts/security/pausable/Pausable.sol index 81689501..f1932c64 100644 --- a/contracts/security/pausable/Pausable.sol +++ b/contracts/security/pausable/Pausable.sol @@ -2,13 +2,14 @@ pragma solidity ^0.8.24; +import { Context } from '../../meta/Context.sol'; import { IPausable } from './IPausable.sol'; import { _Pausable } from './_Pausable.sol'; /** * @title Pausable security control module. */ -abstract contract Pausable is IPausable, _Pausable { +abstract contract Pausable is IPausable, _Pausable, Context { /** * @inheritdoc IPausable */ diff --git a/contracts/security/pausable/_IPausable.sol b/contracts/security/pausable/_IPausable.sol index 5bb9f376..c5505bc6 100644 --- a/contracts/security/pausable/_IPausable.sol +++ b/contracts/security/pausable/_IPausable.sol @@ -2,7 +2,9 @@ pragma solidity ^0.8.24; -interface _IPausable { +import { _IContext } from '../../meta/_IContext.sol'; + +interface _IPausable is _IContext { error Pausable__Paused(); error Pausable__NotPaused(); diff --git a/contracts/security/pausable/_Pausable.sol b/contracts/security/pausable/_Pausable.sol index 37d5f0ae..b90b8bcf 100644 --- a/contracts/security/pausable/_Pausable.sol +++ b/contracts/security/pausable/_Pausable.sol @@ -3,12 +3,13 @@ pragma solidity ^0.8.24; import { PausableStorage } from '../../storage/PausableStorage.sol'; +import { _Context } from '../../meta/_Context.sol'; import { _IPausable } from './_IPausable.sol'; /** * @title Internal functions for Pausable security control module. */ -abstract contract _Pausable is _IPausable { +abstract contract _Pausable is _IPausable, _Context { modifier whenNotPaused() { if (_paused()) revert Pausable__Paused(); _; @@ -36,7 +37,7 @@ abstract contract _Pausable is _IPausable { PausableStorage .layout(PausableStorage.DEFAULT_STORAGE_SLOT) .paused = true; - emit Paused(msg.sender); + emit Paused(_msgSender()); } /** @@ -46,6 +47,6 @@ abstract contract _Pausable is _IPausable { delete PausableStorage .layout(PausableStorage.DEFAULT_STORAGE_SLOT) .paused; - emit Unpaused(msg.sender); + emit Unpaused(_msgSender()); } } diff --git a/contracts/storage/ERC2771Storage.sol b/contracts/storage/ERC2771Storage.sol new file mode 100644 index 00000000..a388cfd5 --- /dev/null +++ b/contracts/storage/ERC2771Storage.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +library ERC2771Storage { + /** + * @custom:storage-location erc7201:solidstate.contracts.storage.ERC2771 + */ + struct Layout { + mapping(address account => bool trustedStatus) trustedForwarders; + } + + bytes32 internal constant DEFAULT_STORAGE_SLOT = + keccak256( + abi.encode( + uint256( + keccak256(bytes('solidstate.contracts.storage.ERC2771')) + ) - 1 + ) + ) & ~bytes32(uint256(0xff)); + + function layout() internal pure returns (Layout storage $) { + $ = layout(DEFAULT_STORAGE_SLOT); + } + + function layout(bytes32 slot) internal pure returns (Layout storage $) { + assembly { + $.slot := slot + } + } +} diff --git a/contracts/token/fungible/FungibleToken.sol b/contracts/token/fungible/FungibleToken.sol index 31ea5d62..5cd3cc01 100644 --- a/contracts/token/fungible/FungibleToken.sol +++ b/contracts/token/fungible/FungibleToken.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.24; import { IERC20 } from '../../interfaces/IERC20.sol'; +import { Context } from '../../meta/Context.sol'; import { ERC20Storage } from '../../storage/ERC20Storage.sol'; import { IFungibleToken } from './IFungibleToken.sol'; import { _FungibleToken } from './_FungibleToken.sol'; @@ -10,7 +11,7 @@ import { _FungibleToken } from './_FungibleToken.sol'; /** * @title Base FungibleToken implementation, excluding optional extensions */ -abstract contract FungibleToken is IFungibleToken, _FungibleToken { +abstract contract FungibleToken is IFungibleToken, _FungibleToken, Context { /** * @inheritdoc IERC20 */ diff --git a/contracts/token/fungible/IFungibleToken.sol b/contracts/token/fungible/IFungibleToken.sol index 3b4556dc..c5b958c5 100644 --- a/contracts/token/fungible/IFungibleToken.sol +++ b/contracts/token/fungible/IFungibleToken.sol @@ -3,9 +3,10 @@ pragma solidity ^0.8.24; import { IERC20 } from '../../interfaces/IERC20.sol'; +import { IContext } from '../../meta/IContext.sol'; import { _IFungibleToken } from './_IFungibleToken.sol'; /** * @title FungibleToken base interface */ -interface IFungibleToken is _IFungibleToken, IERC20 {} +interface IFungibleToken is _IFungibleToken, IERC20, IContext {} diff --git a/contracts/token/fungible/_FungibleToken.sol b/contracts/token/fungible/_FungibleToken.sol index 21ac067c..46f19741 100644 --- a/contracts/token/fungible/_FungibleToken.sol +++ b/contracts/token/fungible/_FungibleToken.sol @@ -3,12 +3,13 @@ pragma solidity ^0.8.24; import { ERC20Storage } from '../../storage/ERC20Storage.sol'; +import { _Context } from '../../meta/_Context.sol'; import { _IFungibleToken } from './_IFungibleToken.sol'; /** * @title Base FungibleToken internal functions, excluding optional extensions */ -abstract contract _FungibleToken is _IFungibleToken { +abstract contract _FungibleToken is _IFungibleToken, _Context { /** * @notice query the total minted token supply * @return token supply @@ -49,7 +50,7 @@ abstract contract _FungibleToken is _IFungibleToken { } function _approve(address spender, uint256 amount) internal returns (bool) { - return _approve(msg.sender, spender, amount); + return _approve(_msgSender(), spender, amount); } /** @@ -143,7 +144,7 @@ abstract contract _FungibleToken is _IFungibleToken { address recipient, uint256 amount ) internal returns (bool) { - return _transfer(msg.sender, recipient, amount); + return _transfer(_msgSender(), recipient, amount); } /** @@ -193,7 +194,7 @@ abstract contract _FungibleToken is _IFungibleToken { address recipient, uint256 amount ) internal virtual returns (bool) { - _decreaseAllowance(holder, msg.sender, amount); + _decreaseAllowance(holder, _msgSender(), amount); _transfer(holder, recipient, amount); diff --git a/contracts/token/fungible/_IFungibleToken.sol b/contracts/token/fungible/_IFungibleToken.sol index 41a7cfa3..ad2fbacb 100644 --- a/contracts/token/fungible/_IFungibleToken.sol +++ b/contracts/token/fungible/_IFungibleToken.sol @@ -3,11 +3,12 @@ pragma solidity ^0.8.24; import { _IERC20 } from '../../interfaces/_IERC20.sol'; +import { _IContext } from '../../meta/_IContext.sol'; /** * @title FungibleToken base interface */ -interface _IFungibleToken is _IERC20 { +interface _IFungibleToken is _IERC20, _IContext { error FungibleToken__ApproveFromZeroAddress(); error FungibleToken__ApproveToZeroAddress(); error FungibleToken__BurnExceedsBalance(); diff --git a/contracts/token/fungible/extended/_FungibleTokenExtended.sol b/contracts/token/fungible/extended/_FungibleTokenExtended.sol index c4146bb7..07096735 100644 --- a/contracts/token/fungible/extended/_FungibleTokenExtended.sol +++ b/contracts/token/fungible/extended/_FungibleTokenExtended.sol @@ -24,13 +24,13 @@ abstract contract _FungibleTokenExtended is address spender, uint256 amount ) internal virtual returns (bool) { - uint256 allowance = _allowance(msg.sender, spender); + uint256 allowance = _allowance(_msgSender(), spender); unchecked { if (allowance > allowance + amount) revert FungibleTokenExtended__ExcessiveAllowance(); - return _approve(msg.sender, spender, allowance + amount); + return _approve(_msgSender(), spender, allowance + amount); } } @@ -44,7 +44,7 @@ abstract contract _FungibleTokenExtended is address spender, uint256 amount ) internal virtual returns (bool) { - _decreaseAllowance(msg.sender, spender, amount); + _decreaseAllowance(_msgSender(), spender, amount); return true; } diff --git a/contracts/token/fungible/vault/_FungibleVaultToken.sol b/contracts/token/fungible/vault/_FungibleVaultToken.sol index e9b70494..b7070a2e 100644 --- a/contracts/token/fungible/vault/_FungibleVaultToken.sol +++ b/contracts/token/fungible/vault/_FungibleVaultToken.sol @@ -199,7 +199,7 @@ abstract contract _FungibleVaultToken is shareAmount = _previewDeposit(assetAmount); - _deposit(msg.sender, receiver, assetAmount, shareAmount, 0, 0); + _deposit(_msgSender(), receiver, assetAmount, shareAmount, 0, 0); } /** @@ -217,7 +217,7 @@ abstract contract _FungibleVaultToken is assetAmount = _previewMint(shareAmount); - _deposit(msg.sender, receiver, assetAmount, shareAmount, 0, 0); + _deposit(_msgSender(), receiver, assetAmount, shareAmount, 0, 0); } /** @@ -237,7 +237,15 @@ abstract contract _FungibleVaultToken is shareAmount = _previewWithdraw(assetAmount); - _withdraw(msg.sender, receiver, owner, assetAmount, shareAmount, 0, 0); + _withdraw( + _msgSender(), + receiver, + owner, + assetAmount, + shareAmount, + 0, + 0 + ); } /** @@ -257,7 +265,15 @@ abstract contract _FungibleVaultToken is assetAmount = _previewRedeem(shareAmount); - _withdraw(msg.sender, receiver, owner, assetAmount, shareAmount, 0, 0); + _withdraw( + _msgSender(), + receiver, + owner, + assetAmount, + shareAmount, + 0, + 0 + ); } /** diff --git a/contracts/token/multi/IMultiToken.sol b/contracts/token/multi/IMultiToken.sol index 62327694..6e40d2e3 100644 --- a/contracts/token/multi/IMultiToken.sol +++ b/contracts/token/multi/IMultiToken.sol @@ -4,9 +4,10 @@ pragma solidity ^0.8.24; import { IERC1155 } from '../../interfaces/IERC1155.sol'; import { IIntrospectable } from '../../introspection/IIntrospectable.sol'; +import { IContext } from '../../meta/IContext.sol'; import { _IMultiToken } from './_IMultiToken.sol'; /** * @title MultiToken base interface */ -interface IMultiToken is _IMultiToken, IERC1155, IIntrospectable {} +interface IMultiToken is _IMultiToken, IERC1155, IIntrospectable, IContext {} diff --git a/contracts/token/multi/MultiToken.sol b/contracts/token/multi/MultiToken.sol index d9c70c5f..5b025cf3 100644 --- a/contracts/token/multi/MultiToken.sol +++ b/contracts/token/multi/MultiToken.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.24; import { IERC1155 } from '../../interfaces/IERC1155.sol'; import { Introspectable } from '../../introspection/Introspectable.sol'; +import { Context } from '../../meta/Context.sol'; import { IMultiToken } from './IMultiToken.sol'; import { _MultiToken } from './_MultiToken.sol'; @@ -11,7 +12,12 @@ import { _MultiToken } from './_MultiToken.sol'; * @title Base MultiToken contract * @dev derived from https://github.com/OpenZeppelin/openzeppelin-contracts/ (MIT license) */ -abstract contract MultiToken is IMultiToken, _MultiToken, Introspectable { +abstract contract MultiToken is + IMultiToken, + _MultiToken, + Introspectable, + Context +{ /** * @inheritdoc IERC1155 */ diff --git a/contracts/token/multi/_IMultiToken.sol b/contracts/token/multi/_IMultiToken.sol index 54d13570..71a14412 100644 --- a/contracts/token/multi/_IMultiToken.sol +++ b/contracts/token/multi/_IMultiToken.sol @@ -4,11 +4,12 @@ pragma solidity ^0.8.24; import { _IERC1155 } from '../../interfaces/_IERC1155.sol'; import { _IIntrospectable } from '../../introspection/_IIntrospectable.sol'; +import { _IContext } from '../../meta/_IContext.sol'; /** * @title MultiToken base interface */ -interface _IMultiToken is _IERC1155, _IIntrospectable { +interface _IMultiToken is _IERC1155, _IIntrospectable, _IContext { error MultiToken__ArrayLengthMismatch(); error MultiToken__BalanceQueryZeroAddress(); error MultiToken__NotOwnerOrApproved(); diff --git a/contracts/token/multi/_MultiToken.sol b/contracts/token/multi/_MultiToken.sol index 5298cc7d..bfcf17e2 100644 --- a/contracts/token/multi/_MultiToken.sol +++ b/contracts/token/multi/_MultiToken.sol @@ -6,13 +6,14 @@ import { IERC1155Receiver } from '../../interfaces/IERC1155Receiver.sol'; import { _Introspectable } from '../../introspection/_Introspectable.sol'; import { ERC1155Storage } from '../../storage/ERC1155Storage.sol'; import { Address } from '../../utils/Address.sol'; +import { _Context } from '../../meta/_Context.sol'; import { _IMultiToken } from './_IMultiToken.sol'; /** * @title Base MultiToken internal functions * @dev derived from https://github.com/OpenZeppelin/openzeppelin-contracts/ (MIT license) */ -abstract contract _MultiToken is _IMultiToken, _Introspectable { +abstract contract _MultiToken is _IMultiToken, _Introspectable, _Context { using Address for address; /** @@ -83,11 +84,11 @@ abstract contract _MultiToken is _IMultiToken, _Introspectable { address operator, bool status ) internal virtual { - if (msg.sender == operator) revert MultiToken__SelfApproval(); + if (_msgSender() == operator) revert MultiToken__SelfApproval(); ERC1155Storage .layout(ERC1155Storage.DEFAULT_STORAGE_SLOT) - .operatorApprovals[msg.sender][operator] = status; - emit ApprovalForAll(msg.sender, operator, status); + .operatorApprovals[_msgSender()][operator] = status; + emit ApprovalForAll(_msgSender(), operator, status); } /** @@ -107,7 +108,7 @@ abstract contract _MultiToken is _IMultiToken, _Introspectable { if (account == address(0)) revert MultiToken__MintToZeroAddress(); _beforeTokenTransfer( - msg.sender, + _msgSender(), address(0), account, _asSingletonArray(id), @@ -119,7 +120,7 @@ abstract contract _MultiToken is _IMultiToken, _Introspectable { account ] += amount; - emit TransferSingle(msg.sender, address(0), account, id, amount); + emit TransferSingle(_msgSender(), address(0), account, id, amount); } /** @@ -138,7 +139,7 @@ abstract contract _MultiToken is _IMultiToken, _Introspectable { _mint(account, id, amount, data); _doSafeTransferAcceptanceCheck( - msg.sender, + _msgSender(), address(0), account, id, @@ -166,7 +167,7 @@ abstract contract _MultiToken is _IMultiToken, _Introspectable { revert MultiToken__ArrayLengthMismatch(); _beforeTokenTransfer( - msg.sender, + _msgSender(), address(0), account, ids, @@ -186,7 +187,7 @@ abstract contract _MultiToken is _IMultiToken, _Introspectable { } } - emit TransferBatch(msg.sender, address(0), account, ids, amounts); + emit TransferBatch(_msgSender(), address(0), account, ids, amounts); } /** @@ -205,7 +206,7 @@ abstract contract _MultiToken is _IMultiToken, _Introspectable { _mintBatch(account, ids, amounts, data); _doSafeBatchTransferAcceptanceCheck( - msg.sender, + _msgSender(), address(0), account, ids, @@ -228,7 +229,7 @@ abstract contract _MultiToken is _IMultiToken, _Introspectable { if (account == address(0)) revert MultiToken__BurnFromZeroAddress(); _beforeTokenTransfer( - msg.sender, + _msgSender(), account, address(0), _asSingletonArray(id), @@ -246,7 +247,7 @@ abstract contract _MultiToken is _IMultiToken, _Introspectable { balances[account] -= amount; } - emit TransferSingle(msg.sender, account, address(0), id, amount); + emit TransferSingle(_msgSender(), account, address(0), id, amount); } /** @@ -264,7 +265,14 @@ abstract contract _MultiToken is _IMultiToken, _Introspectable { if (ids.length != amounts.length) revert MultiToken__ArrayLengthMismatch(); - _beforeTokenTransfer(msg.sender, account, address(0), ids, amounts, ''); + _beforeTokenTransfer( + _msgSender(), + account, + address(0), + ids, + amounts, + '' + ); mapping(uint256 => mapping(address => uint256)) storage balances = ERC1155Storage @@ -280,7 +288,7 @@ abstract contract _MultiToken is _IMultiToken, _Introspectable { } } - emit TransferBatch(msg.sender, account, address(0), ids, amounts); + emit TransferBatch(_msgSender(), account, address(0), ids, amounts); } function _safeTransferFrom( @@ -290,9 +298,9 @@ abstract contract _MultiToken is _IMultiToken, _Introspectable { uint256 amount, bytes memory data ) internal virtual { - if (from != msg.sender && !_isApprovedForAll(from, msg.sender)) + if (from != _msgSender() && !_isApprovedForAll(from, _msgSender())) revert MultiToken__NotOwnerOrApproved(); - _safeTransfer(msg.sender, from, to, id, amount, data); + _safeTransfer(_msgSender(), from, to, id, amount, data); } function _safeBatchTransferFrom( @@ -302,9 +310,9 @@ abstract contract _MultiToken is _IMultiToken, _Introspectable { uint256[] memory amounts, bytes memory data ) internal virtual { - if (from != msg.sender && !_isApprovedForAll(from, msg.sender)) + if (from != _msgSender() && !_isApprovedForAll(from, _msgSender())) revert MultiToken__NotOwnerOrApproved(); - _safeTransferBatch(msg.sender, from, to, ids, amounts, data); + _safeTransferBatch(_msgSender(), from, to, ids, amounts, data); } /** diff --git a/contracts/token/non_fungible/INonFungibleToken.sol b/contracts/token/non_fungible/INonFungibleToken.sol index 07a660fc..d2675542 100644 --- a/contracts/token/non_fungible/INonFungibleToken.sol +++ b/contracts/token/non_fungible/INonFungibleToken.sol @@ -4,9 +4,15 @@ pragma solidity ^0.8.24; import { IERC721 } from '../../interfaces/IERC721.sol'; import { IIntrospectable } from '../../introspection/IIntrospectable.sol'; +import { IContext } from '../../meta/IContext.sol'; import { _INonFungibleToken } from './_INonFungibleToken.sol'; /** * @title NonFungibleToken base interface */ -interface INonFungibleToken is _INonFungibleToken, IERC721, IIntrospectable {} +interface INonFungibleToken is + _INonFungibleToken, + IERC721, + IIntrospectable, + IContext +{} diff --git a/contracts/token/non_fungible/NonFungibleToken.sol b/contracts/token/non_fungible/NonFungibleToken.sol index 61d4feb0..3bfaa106 100644 --- a/contracts/token/non_fungible/NonFungibleToken.sol +++ b/contracts/token/non_fungible/NonFungibleToken.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.24; import { IERC721 } from '../../interfaces/IERC721.sol'; import { Introspectable } from '../../introspection/Introspectable.sol'; +import { Context } from '../../meta/Context.sol'; import { INonFungibleToken } from './INonFungibleToken.sol'; import { _NonFungibleToken } from './_NonFungibleToken.sol'; @@ -13,7 +14,8 @@ import { _NonFungibleToken } from './_NonFungibleToken.sol'; abstract contract NonFungibleToken is INonFungibleToken, _NonFungibleToken, - Introspectable + Introspectable, + Context { /** * @inheritdoc IERC721 diff --git a/contracts/token/non_fungible/_INonFungibleToken.sol b/contracts/token/non_fungible/_INonFungibleToken.sol index e52636c1..145cef39 100644 --- a/contracts/token/non_fungible/_INonFungibleToken.sol +++ b/contracts/token/non_fungible/_INonFungibleToken.sol @@ -4,11 +4,12 @@ pragma solidity ^0.8.24; import { _IERC721 } from '../../interfaces/_IERC721.sol'; import { _IIntrospectable } from '../../introspection/_IIntrospectable.sol'; +import { _IContext } from '../../meta/_IContext.sol'; /** * @title NonFungibleToken base interface */ -interface _INonFungibleToken is _IERC721, _IIntrospectable { +interface _INonFungibleToken is _IERC721, _IIntrospectable, _IContext { error NonFungibleToken__NotOwnerOrApproved(); error NonFungibleToken__SelfApproval(); error NonFungibleToken__BalanceQueryZeroAddress(); diff --git a/contracts/token/non_fungible/_NonFungibleToken.sol b/contracts/token/non_fungible/_NonFungibleToken.sol index 85a2ad2b..b14fd4ce 100644 --- a/contracts/token/non_fungible/_NonFungibleToken.sol +++ b/contracts/token/non_fungible/_NonFungibleToken.sol @@ -8,12 +8,17 @@ import { IERC721Receiver } from '../../interfaces/IERC721Receiver.sol'; import { _Introspectable } from '../../introspection/_Introspectable.sol'; import { ERC721Storage } from '../../storage/ERC721Storage.sol'; import { Address } from '../../utils/Address.sol'; +import { _Context } from '../../meta/_Context.sol'; import { _INonFungibleToken } from './_INonFungibleToken.sol'; /** * @title Base NonFungibleToken internal functions */ -abstract contract _NonFungibleToken is _INonFungibleToken, _Introspectable { +abstract contract _NonFungibleToken is + _INonFungibleToken, + _Introspectable, + _Context +{ using Address for address; using EnumerableMap for EnumerableMap.UintToAddressMap; using EnumerableSet for EnumerableSet.UintSet; @@ -160,7 +165,7 @@ abstract contract _NonFungibleToken is _INonFungibleToken, _Introspectable { uint256 tokenId ) internal virtual { _handleTransferMessageValue(from, to, tokenId, msg.value); - if (!_isApprovedOrOwner(msg.sender, tokenId)) + if (!_isApprovedOrOwner(_msgSender(), tokenId)) revert NonFungibleToken__NotOwnerOrApproved(); _transfer(from, to, tokenId); } @@ -191,7 +196,7 @@ abstract contract _NonFungibleToken is _INonFungibleToken, _Introspectable { bytes memory data ) internal virtual { _handleTransferMessageValue(from, to, tokenId, msg.value); - if (!_isApprovedOrOwner(msg.sender, tokenId)) + if (!_isApprovedOrOwner(_msgSender(), tokenId)) revert NonFungibleToken__NotOwnerOrApproved(); _safeTransfer(from, to, tokenId, data); } @@ -202,7 +207,7 @@ abstract contract _NonFungibleToken is _INonFungibleToken, _Introspectable { address owner = _ownerOf(tokenId); if (operator == owner) revert NonFungibleToken__SelfApproval(); - if (msg.sender != owner && !_isApprovedForAll(owner, msg.sender)) + if (_msgSender() != owner && !_isApprovedForAll(owner, _msgSender())) revert NonFungibleToken__NotOwnerOrApproved(); ERC721Storage.layout(ERC721Storage.DEFAULT_STORAGE_SLOT).tokenApprovals[ @@ -215,11 +220,11 @@ abstract contract _NonFungibleToken is _INonFungibleToken, _Introspectable { address operator, bool status ) internal virtual { - if (operator == msg.sender) revert NonFungibleToken__SelfApproval(); + if (operator == _msgSender()) revert NonFungibleToken__SelfApproval(); ERC721Storage .layout(ERC721Storage.DEFAULT_STORAGE_SLOT) - .operatorApprovals[msg.sender][operator] = status; - emit ApprovalForAll(msg.sender, operator, status); + .operatorApprovals[_msgSender()][operator] = status; + emit ApprovalForAll(_msgSender(), operator, status); } function _checkOnERC721Received( @@ -235,7 +240,7 @@ abstract contract _NonFungibleToken is _INonFungibleToken, _Introspectable { bytes memory returnData = to.functionCall( abi.encodeWithSelector( IERC721Receiver(to).onERC721Received.selector, - msg.sender, + _msgSender(), from, tokenId, data diff --git a/tasks/generate_eip_712.ts b/tasks/generate_eip_712.ts index 6461e15a..1b269d9c 100644 --- a/tasks/generate_eip_712.ts +++ b/tasks/generate_eip_712.ts @@ -7,7 +7,7 @@ const name = 'EIP712'; const filepath = 'cryptography'; const TEMPLATE_SOL = ` -pragma solidity ^0.8.20; +pragma solidity ^0.8.24; /** * @title Procedurally generated EIP-712 typed structured data hashing and signing library diff --git a/test/meta/Context.ts b/test/meta/Context.ts new file mode 100644 index 00000000..872a4f75 --- /dev/null +++ b/test/meta/Context.ts @@ -0,0 +1,54 @@ +import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers'; +import { $Context, $Context__factory } from '@solidstate/typechain-types'; +import { expect } from 'chai'; +import { ethers } from 'hardhat'; + +describe('Context', () => { + let instance: $Context; + let deployer: SignerWithAddress; + + beforeEach(async () => { + [deployer] = await ethers.getSigners(); + instance = await new $Context__factory(deployer).deploy(); + }); + + describe('__internal', () => { + describe('#_msgSender()', () => { + it('returns message sender', async () => { + const tx = await instance.$_msgSender.populateTransaction(); + tx.data = ethers.concat([tx.data, ethers.randomBytes(20)]); + + const result = await deployer.call(tx); + const decoded = instance.interface.decodeFunctionResult( + '$_msgSender', + result, + ); + + expect(decoded).to.deep.equal([await deployer.getAddress()]); + }); + }); + + describe('#_msgData()', () => { + it('returns complete message data', async () => { + const tx = await instance.$_msgData.populateTransaction(); + tx.data = ethers.concat([tx.data, ethers.randomBytes(20)]); + + // message data is returned as received, demonstrating the malleability of msg.data + + const result = await deployer.call(tx); + const decoded = instance.interface.decodeFunctionResult( + '$_msgData', + result, + ); + + expect(decoded).to.deep.equal([tx.data]); + }); + }); + + describe('#_calldataSuffixLength()', () => { + it('returns 0', async () => { + expect(await instance.$_calldataSuffixLength.staticCall()).to.equal(0n); + }); + }); + }); +}); diff --git a/test/meta/ForwardedMetaTransactionContext.ts b/test/meta/ForwardedMetaTransactionContext.ts new file mode 100644 index 00000000..23411563 --- /dev/null +++ b/test/meta/ForwardedMetaTransactionContext.ts @@ -0,0 +1,180 @@ +import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers'; +import { + $ForwardedMetaTransactionContext, + $ForwardedMetaTransactionContext__factory, +} from '@solidstate/typechain-types'; +import { TypedContractMethod } from '@solidstate/typechain-types/common'; +import { expect } from 'chai'; +import { BytesLike, ContractMethodArgs } from 'ethers'; +import { ethers } from 'hardhat'; + +const callMetaTransaction = async ( + signer: SignerWithAddress, + fn: TypedContractMethod<[], [string], 'nonpayable' | 'payable' | 'view'>, + data: BytesLike, + args: ContractMethodArgs<[]> = [], +) => { + const tx = await fn.populateTransaction(...args); + tx.data = ethers.concat([tx.data, data]); + + const result = await signer.call(tx); + + return new ethers.Interface([fn.fragment]).decodeFunctionResult( + fn.name, + result, + ); +}; + +describe('ForwardedMetaTransactionContext', () => { + let instance: $ForwardedMetaTransactionContext; + let trustedForwarder: SignerWithAddress; + let nonTrustedForwarder: SignerWithAddress; + + beforeEach(async () => { + let deployer; + [deployer, trustedForwarder, nonTrustedForwarder] = + await ethers.getSigners(); + instance = await new $ForwardedMetaTransactionContext__factory( + deployer, + ).deploy(); + + await instance.$_addTrustedForwarder(await trustedForwarder.getAddress()); + }); + + // TODO: spec + + describe('__internal', () => { + describe('#_msgSender()', () => { + it('returns forwarded sender if sender is trusted forwarder', async () => { + const forwardedAddress = ethers.hexlify(ethers.randomBytes(20)); + + expect( + await callMetaTransaction( + trustedForwarder, + instance.$_msgSender, + forwardedAddress, + ), + ).to.deep.equal([forwardedAddress]); + }); + + it('returns message sender if sender is not trusted forwarder', async () => { + expect( + await callMetaTransaction( + nonTrustedForwarder, + instance.$_msgSender, + ethers.randomBytes(20), + ), + ).to.deep.equal([await nonTrustedForwarder.getAddress()]); + }); + + it('returns message sender if message data length is less than suffix length', async () => { + // account for 4-byte selector when calculting suffix length + const suffix = ethers.randomBytes( + Number( + (await instance.$_calldataSuffixLength.staticCall()) - 4n - 1n, + ), + ); + + expect( + await callMetaTransaction( + trustedForwarder, + instance.$_msgSender, + suffix, + ), + ).to.deep.equal([await trustedForwarder.getAddress()]); + }); + }); + + describe('#_msgData()', () => { + it('returns message data without suffix if sender is trusted forwarder', async () => { + const nonSuffixedData = instance.$_msgData.fragment.selector; + + expect( + await callMetaTransaction( + trustedForwarder, + instance.$_msgData, + ethers.randomBytes(20), + ), + ).to.deep.equal([nonSuffixedData]); + }); + + it('returns complete message data if sender is not trusted forwarder', async () => { + const nonSuffixedData = instance.$_msgData.fragment.selector; + const data = ethers.randomBytes(20); + + // message data is returned as received, demonstrating the malleability of msg.data + + expect( + await callMetaTransaction( + nonTrustedForwarder, + instance.$_msgData, + data, + ), + ).to.deep.equal([ethers.concat([nonSuffixedData, data])]); + }); + + it('returns complete message data if message data length is less than suffix length', async () => { + const nonSuffixedData = instance.$_msgData.fragment.selector; + // account for 4-byte selector when calculting suffix length + const suffix = ethers.randomBytes( + Number( + (await instance.$_calldataSuffixLength.staticCall()) - 4n - 1n, + ), + ); + + // message data is returned as received, demonstrating the malleability of msg.data + + expect( + await callMetaTransaction( + trustedForwarder, + instance.$_msgData, + suffix, + ), + ).to.deep.equal([ethers.concat([nonSuffixedData, suffix])]); + }); + }); + + describe('#_calldataSuffixLength()', () => { + it('returns 20', async () => { + expect(await instance.$_calldataSuffixLength.staticCall()).to.equal( + 20n, + ); + }); + }); + + describe('#_isTrustedForwarder(address)', () => { + it('returns trusted forwarder status of account', async () => { + expect( + await instance.$_isTrustedForwarder( + await nonTrustedForwarder.getAddress(), + ), + ).to.be.false; + + expect( + await instance.$_isTrustedForwarder( + await trustedForwarder.getAddress(), + ), + ).to.be.true; + }); + }); + + describe('#_addTrustedForwarder(address)', () => { + it('grants trusted forwarder status to account', async () => { + await instance.$_addTrustedForwarder(await instance.getAddress()); + + expect(await instance.$_isTrustedForwarder(await instance.getAddress())) + .to.be.true; + }); + }); + + describe('#_removeTrustedForwarder(address)', () => { + it('revokes trusted forwarder status from account', async () => { + await instance.$_addTrustedForwarder(await instance.getAddress()); + await instance.$_removeTrustedForwarder(await instance.getAddress()); + + expect(await instance.$_isTrustedForwarder(await instance.getAddress())) + .to.be.false; + }); + }); + }); +});