diff --git a/src/tokens/ERC20P/EIP2612.sol b/src/tokens/ERC20P/EIP2612.sol new file mode 100644 index 000000000..72b653780 --- /dev/null +++ b/src/tokens/ERC20P/EIP2612.sol @@ -0,0 +1,175 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import {ERC20StorageLayout} from "./ERC20StorageLayout.sol"; + +/// @notice EIP-2612 implementation implementation. +/// @author Solady: +/// (https://github.com/vectorized/solady/blob/main/src/tokens/ERC20P/ERC20StorageLayout.sol) +/// +/// @dev Note: +/// - The `permit` function uses the ecrecover precompile (0x1). +abstract contract EIP2612 is ERC20StorageLayout { + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CUSTOM ERRORS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev The permit is invalid. + error InvalidPermit(); + + /// @dev The permit has expired. + error PermitExpired(); + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CONSTANTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + /// @dev The nonce slot of `owner` is given by: + /// ``` + /// mstore(0x0c, _NONCES_SLOT_SEED) + /// mstore(0x00, owner) + /// let nonceSlot := keccak256(0x0c, 0x20) + /// ``` + uint256 internal constant _NONCES_SLOT_SEED = 0x38377508; + + /// @dev `(_NONCES_SLOT_SEED << 16) | 0x1901`. + uint256 private constant _NONCES_SLOT_SEED_WITH_SIGNATURE_PREFIX = + 0x383775081901; + + /// @dev `keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")`. + bytes32 private constant _DOMAIN_TYPEHASH = + 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f; + + /// @dev `keccak256("1")`. + bytes32 private constant _VERSION_HASH = + 0xc89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc6; + + /// @dev `keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)")`. + bytes32 private constant _PERMIT_TYPEHASH = + 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9; + + function name() public view virtual returns (string memory); + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* EIP-2612 */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev For more performance, override to return the constant value + /// of `keccak256(bytes(name()))` if `name()` will never change. + function _constantNameHash() + internal + view + virtual + returns (bytes32 result) {} + + /// @dev Returns the current nonce for `owner`. + /// This value is used to compute the signature for EIP-2612 permit. + function nonces( + address owner + ) public view virtual returns (uint256 result) { + /// @solidity memory-safe-assembly + assembly { + // Compute the nonce slot and load its value. + mstore(0x0c, _NONCES_SLOT_SEED) + mstore(0x00, owner) + result := sload(keccak256(0x0c, 0x20)) + } + } + + /// @dev Sets `value` as the allowance of `spender` over the tokens of `owner`, + /// authorized by a signed approval by `owner`. + /// + /// Emits a {Approval} event. + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual { + bytes32 nameHash = _constantNameHash(); + // We simply calculate it on-the-fly to allow for cases where the `name` may change. + if (nameHash == bytes32(0)) nameHash = keccak256(bytes(name())); + /// @solidity memory-safe-assembly + assembly { + // Revert if the block timestamp is greater than `deadline`. + if gt(timestamp(), deadline) { + mstore(0x00, 0x1a15a3cc) // `PermitExpired()`. + revert(0x1c, 0x04) + } + let m := mload(0x40) // Grab the free memory pointer. + // Clean the upper 96 bits. + owner := shr(96, shl(96, owner)) + spender := shr(96, shl(96, spender)) + // Compute the nonce slot and load its value. + mstore(0x0e, _NONCES_SLOT_SEED_WITH_SIGNATURE_PREFIX) + mstore(0x00, owner) + let nonceSlot := keccak256(0x0c, 0x20) + let nonceValue := sload(nonceSlot) + // Prepare the domain separator. + mstore(m, _DOMAIN_TYPEHASH) + mstore(add(m, 0x20), nameHash) + mstore(add(m, 0x40), _VERSION_HASH) + mstore(add(m, 0x60), chainid()) + mstore(add(m, 0x80), address()) + mstore(0x2e, keccak256(m, 0xa0)) + // Prepare the struct hash. + mstore(m, _PERMIT_TYPEHASH) + mstore(add(m, 0x20), owner) + mstore(add(m, 0x40), spender) + mstore(add(m, 0x60), value) + mstore(add(m, 0x80), nonceValue) + mstore(add(m, 0xa0), deadline) + mstore(0x4e, keccak256(m, 0xc0)) + // Prepare the ecrecover calldata. + mstore(0x00, keccak256(0x2c, 0x42)) + mstore(0x20, and(0xff, v)) + mstore(0x40, r) + mstore(0x60, s) + let t := staticcall(gas(), 1, 0, 0x80, 0x20, 0x20) + // If the ecrecover fails, the returndatasize will be 0x00, + // `owner` will be checked if it equals the hash at 0x00, + // which evaluates to false (i.e. 0), and we will revert. + // If the ecrecover succeeds, the returndatasize will be 0x20, + // `owner` will be compared against the returned address at 0x20. + if iszero(eq(mload(returndatasize()), owner)) { + mstore(0x00, 0xddafbaef) // `InvalidPermit()`. + revert(0x1c, 0x04) + } + // Increment and store the updated nonce. + sstore(nonceSlot, add(nonceValue, t)) // `t` is 1 if ecrecover succeeds. + // Compute the allowance slot and store the value. + // The `owner` is already at slot 0x20. + mstore(0x40, or(shl(160, _ALLOWANCE_SLOT_SEED), spender)) + sstore(keccak256(0x2c, 0x34), value) + // Emit the {Approval} event. + log3( + add(m, 0x60), + 0x20, + 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925, + owner, + spender + ) + mstore(0x40, m) // Restore the free memory pointer. + mstore(0x60, 0) // Restore the zero pointer. + } + } + + /// @dev Returns the EIP-712 domain separator for the EIP-2612 permit. + function DOMAIN_SEPARATOR() public view virtual returns (bytes32 result) { + bytes32 nameHash = _constantNameHash(); + // We simply calculate it on-the-fly to allow for cases where the `name` may change. + if (nameHash == bytes32(0)) nameHash = keccak256(bytes(name())); + /// @solidity memory-safe-assembly + assembly { + let m := mload(0x40) // Grab the free memory pointer. + mstore(m, _DOMAIN_TYPEHASH) + mstore(add(m, 0x20), nameHash) + mstore(add(m, 0x40), _VERSION_HASH) + mstore(add(m, 0x60), chainid()) + mstore(add(m, 0x80), address()) + result := keccak256(m, 0xa0) + } + } +} diff --git a/src/tokens/ERC20P/ERC20P.sol b/src/tokens/ERC20P/ERC20P.sol new file mode 100644 index 000000000..2b6bb7839 --- /dev/null +++ b/src/tokens/ERC20P/ERC20P.sol @@ -0,0 +1,503 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import {ERC20StorageLayout} from "./ERC20StorageLayout.sol"; + +/// @notice ERC20 implementation with addition of packed extra-data. +/// @author Solady: +/// (https://github.com/vectorized/solady/blob/main/src/tokens/ERC20P/ERC20P.sol) +/// @author Modified from Solmate: +/// (https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC20.sol) +/// @author Modified from OpenZeppelin: +/// (https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol) +/// +/// @dev Note: +/// - The ERC20 standard allows minting and transferring to and from +/// the zero address, minting and transferring zero tokens, +/// as well as self-approvals. +/// For performance, this implementation WILL NOT revert for such actions. +/// Please add any checks with overrides if desired. +/// - Current implementation allows max balance up to 2^128-1, +/// allowing in the same time an extra data of the same size. +/// As a result, please avoid any possibilities of encountering such a case, +/// consider capping the supply somehow. +/// +/// If you are overriding: +/// - NEVER violate the ERC20 invariant: +/// the total sum of all balances must be equal to `totalSupply()`. +/// - Check that the overridden function is actually used in +/// the function you want to change the behavior of. +/// Much of the code has been manually inlined for performance. +/// - NEVER use 0x38377508 as storage slot seed if implementing +/// EIP2612 from this same folder (ERC20P). +/// - NEVER reach more than max supply if defining custom +/// balance/extra-data as thoe are packed in the same slot. +abstract contract ERC20P is ERC20StorageLayout { + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CUSTOM ERRORS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev The total supply has overflowed. + error TotalSupplyOverflow(); + + /// @dev The allowance has overflowed. + error AllowanceOverflow(); + + /// @dev The allowance has underflowed. + error AllowanceUnderflow(); + + /// @dev Insufficient balance. + error InsufficientBalance(); + + /// @dev Insufficient allowance. + error InsufficientAllowance(); + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* EVENTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Emitted when `amount` tokens is transferred from `from` to `to`. + event Transfer(address indexed from, address indexed to, uint256 amount); + + /// @dev Emitted when `amount` tokens is approved by `owner` to be used by `spender`. + event Approval( + address indexed owner, + address indexed spender, + uint256 amount + ); + + /// @dev `keccak256(bytes("Transfer(address,address,uint256)"))`. + uint256 private constant _TRANSFER_EVENT_SIGNATURE = + 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef; + + /// @dev `keccak256(bytes("Approval(address,address,uint256)"))`. + uint256 private constant _APPROVAL_EVENT_SIGNATURE = + 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925; + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* ERC20 METADATA */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Returns the name of the token. + function name() public view virtual returns (string memory); + + /// @dev Returns the symbol of the token. + function symbol() public view virtual returns (string memory); + + /// @dev Returns the decimals places of the token. + function decimals() public view virtual returns (uint8) { + return 18; + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* ERC20 */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Returns the amount of tokens in existence. + function totalSupply() public view virtual returns (uint256 result) { + /// @solidity memory-safe-assembly + assembly { + result := sload(_TOTAL_SUPPLY_SLOT) + } + } + + /// @dev Returns the amount of tokens owned by `owner`. + function balanceOf( + address owner + ) public view virtual returns (uint256 result) { + /// @solidity memory-safe-assembly + assembly { + mstore(0x0c, _PACKED_BALANCE_SLOT_SEED) + mstore(0x00, owner) + result := shr( + _PACKED_POS, + sload(keccak256(0x0c, 0x20)) + ) + } + } + + /// @dev Returns the amount of tokens that `spender` can spend on behalf of `owner`. + function allowance(address owner, address spender) + public + view + virtual + returns (uint256 result) + { + /// @solidity memory-safe-assembly + assembly { + mstore(0x20, spender) + mstore(0x0c, _ALLOWANCE_SLOT_SEED) + mstore(0x00, owner) + result := sload(keccak256(0x0c, 0x34)) + } + } + + /// @dev Sets `amount` as the allowance of `spender` over the caller's tokens. + /// + /// Emits a {Approval} event. + function approve( + address spender, + uint256 amount + ) public virtual returns (bool) { + /// @solidity memory-safe-assembly + assembly { + // Compute the allowance slot and store the amount. + mstore(0x20, spender) + mstore(0x0c, _ALLOWANCE_SLOT_SEED) + mstore(0x00, caller()) + sstore(keccak256(0x0c, 0x34), amount) + // Emit the {Approval} event. + mstore(0x00, amount) + log3(0x00, 0x20, _APPROVAL_EVENT_SIGNATURE, caller(), shr(96, mload(0x2c))) + } + return true; + } + + /// @dev Transfer `amount` tokens from the caller to `to`. + /// + /// Requirements: + /// - `from` must at least have `amount`. + /// + /// Emits a {Transfer} event. + function transfer( + address to, + uint256 amount + ) public virtual returns (bool) { + _beforeTokenTransfer(msg.sender, to, amount); + /// @solidity memory-safe-assembly + assembly { + // Compute the balance slot and load its value. + mstore(0x0c, _PACKED_BALANCE_SLOT_SEED) + mstore(0x00, caller()) + let fromBalanceSlot := keccak256(0x0c, 0x20) + let fromBalance := sload(fromBalanceSlot) + let change := shl(_PACKED_POS, amount) + // Revert if insufficient balance. + if gt(change, fromBalance) { + mstore(0x00, 0xf4d678b8) // `InsufficientBalance()`. + revert(0x1c, 0x04) + } + // Subtract and store the updated balance. + sstore(fromBalanceSlot, sub(fromBalance, change)) + // Compute the balance slot of `to`. + mstore(0x00, to) + let toBalanceSlot := keccak256(0x0c, 0x20) + // Add and store the updated balance of `to`. + // Will not overflow because the sum of all user balances + // cannot exceed the maximum uint256 value. + sstore(toBalanceSlot, add(sload(toBalanceSlot), change)) + // Emit the {Transfer} event. + mstore(0x20, amount) + log3( + 0x20, + 0x20, + _TRANSFER_EVENT_SIGNATURE, + caller(), + shr(96, mload(0x0c)) + ) + } + _afterTokenTransfer(msg.sender, to, amount); + return true; + } + + /// @dev Transfers `amount` tokens from `from` to `to`. + /// + /// Note: Does not update the allowance if it is the maximum uint256 value. + /// + /// Requirements: + /// - `from` must at least have `amount`. + /// - The caller must have at least `amount` of allowance to transfer the tokens of `from`. + /// + /// Emits a {Transfer} event. + function transferFrom( + address from, + address to, + uint256 amount + ) public virtual returns (bool) { + _beforeTokenTransfer(from, to, amount); + /// @solidity memory-safe-assembly + assembly { + let from_ := shl(96, from) + // Compute the allowance slot and load its value. + mstore(0x20, caller()) + mstore(0x0c, or(from_, _ALLOWANCE_SLOT_SEED)) + let allowanceSlot := keccak256(0x0c, 0x34) + let allowance_ := sload(allowanceSlot) + // If the allowance is not the maximum uint256 value. + if add(allowance_, 1) { + // Revert if the amount to be transferred exceeds the allowance. + if gt(amount, allowance_) { + mstore(0x00, 0x13be252b) // `InsufficientAllowance()`. + revert(0x1c, 0x04) + } + // Subtract and store the updated allowance. + sstore(allowanceSlot, sub(allowance_, amount)) + } + // Compute the balance slot and load its value. + mstore(0x0c, or(from_, _PACKED_BALANCE_SLOT_SEED)) + let fromBalanceSlot := keccak256(0x0c, 0x20) + let fromBalance := sload(fromBalanceSlot) + let change := shl(_PACKED_POS, amount) + // Revert if insufficient balance. + if gt(change, fromBalance) { + mstore(0x00, 0xf4d678b8) // `InsufficientBalance()`. + revert(0x1c, 0x04) + } + // Subtract and store the updated balance. + sstore(fromBalanceSlot, sub(fromBalance, change)) + // Compute the balance slot of `to`. + mstore(0x00, to) + let toBalanceSlot := keccak256(0x0c, 0x20) + // Add and store the updated balance of `to`. + // Will not overflow because the sum of all user balances + // cannot exceed the maximum uint256 value. + sstore(toBalanceSlot, add(sload(toBalanceSlot), change)) + // Emit the {Transfer} event. + mstore(0x20, amount) + log3( + 0x20, + 0x20, + _TRANSFER_EVENT_SIGNATURE, + shr(96, from_), + shr(96, mload(0x0c)) + ) + } + _afterTokenTransfer(from, to, amount); + return true; + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* INTERNAL MINT FUNCTIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Mints `amount` tokens to `to`, increasing the total supply. + /// + /// Emits a {Transfer} event. + function _mint(address to, uint256 amount) internal virtual { + _beforeTokenTransfer(address(0), to, amount); + /// @solidity memory-safe-assembly + assembly { + let totalSupplyBefore := sload(_TOTAL_SUPPLY_SLOT) + let totalSupplyAfter := add(totalSupplyBefore, amount) + // Revert if the total supply overflows. + if lt(totalSupplyAfter, totalSupplyBefore) { + mstore(0x00, 0xe5cfe957) // `TotalSupplyOverflow()`. + revert(0x1c, 0x04) + } + // Store the updated total supply. + sstore(_TOTAL_SUPPLY_SLOT, totalSupplyAfter) + // Compute the balance slot and load its value. + mstore(0x0c, _PACKED_BALANCE_SLOT_SEED) + mstore(0x00, to) + let toBalanceSlot := keccak256(0x0c, 0x20) + let change := shl(_PACKED_POS, amount) + // Add and store the updated balance. + sstore(toBalanceSlot, add(sload(toBalanceSlot), change)) + // Emit the {Transfer} event. + mstore(0x20, amount) + log3( + 0x20, + 0x20, + _TRANSFER_EVENT_SIGNATURE, + 0, + shr(96, mload(0x0c)) + ) + } + _afterTokenTransfer(address(0), to, amount); + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* INTERNAL BURN FUNCTIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Burns `amount` tokens from `from`, reducing the total supply. + /// + /// Emits a {Transfer} event. + function _burn(address from, uint256 amount) internal virtual { + _beforeTokenTransfer(from, address(0), amount); + /// @solidity memory-safe-assembly + assembly { + // Compute the balance slot and load its value. + mstore(0x0c, _PACKED_BALANCE_SLOT_SEED) + mstore(0x00, from) + let fromBalanceSlot := keccak256(0x0c, 0x20) + let fromBalance := sload(fromBalanceSlot) + let change := shl(_PACKED_POS, amount) + // Revert if insufficient balance. + if gt(change, fromBalance) { + mstore(0x00, 0xf4d678b8) // `InsufficientBalance()`. + revert(0x1c, 0x04) + } + // Subtract and store the updated balance. + sstore(fromBalanceSlot, sub(fromBalance, change)) + // Subtract and store the updated total supply. + sstore(_TOTAL_SUPPLY_SLOT, sub(sload(_TOTAL_SUPPLY_SLOT), amount)) + // Emit the {Transfer} event. + mstore(0x00, amount) + log3( + 0x00, + 0x20, + _TRANSFER_EVENT_SIGNATURE, + shr(96, shl(96, from)), + 0 + ) + } + _afterTokenTransfer(from, address(0), amount); + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* INTERNAL TRANSFER FUNCTIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Moves `amount` of tokens from `from` to `to`. + function _transfer( + address from, + address to, + uint256 amount + ) internal virtual { + _beforeTokenTransfer(from, to, amount); + /// @solidity memory-safe-assembly + assembly { + let from_ := shl(96, from) + // Compute the balance slot and load its value. + mstore(0x0c, or(from_, _PACKED_BALANCE_SLOT_SEED)) + let fromBalanceSlot := keccak256(0x0c, 0x20) + let fromBalance := sload(fromBalanceSlot) + let change := shl(_PACKED_POS, amount) + // Revert if insufficient balance. + if gt(change, fromBalance) { + mstore(0x00, 0xf4d678b8) // `InsufficientBalance()`. + revert(0x1c, 0x04) + } + // Subtract and store the updated balance. + sstore(fromBalanceSlot, sub(fromBalance, change)) + // Compute the balance slot of `to`. + mstore(0x00, to) + let toBalanceSlot := keccak256(0x0c, 0x20) + // Add and store the updated balance of `to`. + // Will not overflow because the sum of all user balances + // cannot exceed the maximum uint256 value. + sstore(toBalanceSlot, add(sload(toBalanceSlot), change)) + // Emit the {Transfer} event. + mstore(0x20, amount) + log3( + 0x20, + 0x20, + _TRANSFER_EVENT_SIGNATURE, + shr(96, from_), + shr(96, mload(0x0c)) + ) + } + _afterTokenTransfer(from, to, amount); + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* INTERNAL ALLOWANCE FUNCTIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Updates the allowance of `owner` for `spender` based on spent `amount`. + function _spendAllowance( + address owner, + address spender, + uint256 amount + ) internal virtual { + /// @solidity memory-safe-assembly + assembly { + // Compute the allowance slot and load its value. + mstore(0x20, spender) + mstore(0x0c, _ALLOWANCE_SLOT_SEED) + mstore(0x00, owner) + let allowanceSlot := keccak256(0x0c, 0x34) + let allowance_ := sload(allowanceSlot) + // If the allowance is not the maximum uint256 value. + if add(allowance_, 1) { + // Revert if the amount to be transferred exceeds the allowance. + if gt(amount, allowance_) { + mstore(0x00, 0x13be252b) // `InsufficientAllowance()`. + revert(0x1c, 0x04) + } + // Subtract and store the updated allowance. + sstore(allowanceSlot, sub(allowance_, amount)) + } + } + } + + /// @dev Sets `amount` as the allowance of `spender` over the tokens of `owner`. + /// + /// Emits a {Approval} event. + function _approve( + address owner, + address spender, + uint256 amount + ) internal virtual { + /// @solidity memory-safe-assembly + assembly { + let owner_ := shl(96, owner) + // Compute the allowance slot and store the amount. + mstore(0x20, spender) + mstore(0x0c, or(owner_, _ALLOWANCE_SLOT_SEED)) + sstore(keccak256(0x0c, 0x34), amount) + // Emit the {Approval} event. + mstore(0x00, amount) + log3( + 0x00, + 0x20, + _APPROVAL_EVENT_SIGNATURE, + shr(96, owner_), + shr(96, mload(0x2c)) + ) + } + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* INTERNAL EXTRA FUNCTIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Returns packed allowance extra data + function _balanceExtraData( + address owner + ) internal view returns (uint128 data) { + /// @solidity memory-safe-assembly + assembly { + mstore(0x0c, _PACKED_BALANCE_SLOT_SEED) + mstore(0x00, owner) + data := and( + _PACK_MASK, + sload(keccak256(0x0c, 0x20)) + ) + } + } + + function _setBalanceExtraData(address owner, uint128 data) internal { + /// @solidity memory-safe-assembly + assembly { + mstore(0x0c, _PACKED_BALANCE_SLOT_SEED) + mstore(0x00, owner) + let slot := keccak256(0x0c, 0x20) + sstore( + slot, + or(and(sload(slot), _REV_PACK_MASK), data) + ) + } + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* HOOKS TO OVERRIDE */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Hook that is called before any transfer of tokens. + /// This includes minting and burning. + function _beforeTokenTransfer( + address from, + address to, + uint256 amount + ) internal virtual {} + + /// @dev Hook that is called after any transfer of tokens. + /// This includes minting and burning. + function _afterTokenTransfer( + address from, + address to, + uint256 amount + ) internal virtual {} +} diff --git a/src/tokens/ERC20P/ERC20StorageLayout.sol b/src/tokens/ERC20P/ERC20StorageLayout.sol new file mode 100644 index 000000000..f12efcb67 --- /dev/null +++ b/src/tokens/ERC20P/ERC20StorageLayout.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +abstract contract ERC20StorageLayout { + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* STORAGE */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev The storage slot for the total supply. + uint256 internal constant _TOTAL_SUPPLY_SLOT = 0x05345cdf77eb68f44c; + + /// @dev The balance slot of `owner` is given by: + /// ``` + /// mstore(0x0c, _PACKED_BALANCE_SLOT_SEED) + /// mstore(0x00, owner) + /// let balanceSlot := keccak256(0x0c, 0x20) + /// ``` + /// Bit mapping: + /// [0..127]: Extra data + /// [128..255]: Balance + uint256 internal constant _PACKED_BALANCE_SLOT_SEED = 0x87a211a2; + + /// @dev The allowance slot of (`owner`, `spender`) is given by: + /// ``` + /// mstore(0x20, spender) + /// mstore(0x0c, _ALLOWANCE_SLOT_SEED) + /// mstore(0x00, owner) + /// let allowanceSlot := keccak256(0x0c, 0x34) + /// ``` + uint256 internal constant _ALLOWANCE_SLOT_SEED = 0x7f5e9f20; + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CONSTANTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Below you can customize the value accordingly with below value, + /// such that: + /// _PACKED_POS = end position of extra-data and start position of balances + /// _PACK_MASK = 2^{pos} - 1 + uint256 internal constant _PACKED_POS = 128; + + /// @dev Below you can customize the value accordingly with above value, + /// such that: + /// _PACK_MASK = 2^{wishedSize} - 1 + /// _PACKED_POS = wishedSize + uint256 internal constant _PACK_MASK = 0xffffffffffffffffffffffffffffffff; + + /// @dev Below you can customize the value accordingly with above value, + /// such that: + /// _REV_PACK_MASK = ~_PACK_MASK + uint256 internal constant _REV_PACK_MASK = + 0xffffffffffffffffffffffffffffffff00000000000000000000000000000000; +} diff --git a/test/ERC20P.t.sol b/test/ERC20P.t.sol new file mode 100644 index 000000000..40cac563c --- /dev/null +++ b/test/ERC20P.t.sol @@ -0,0 +1,508 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import "./utils/SoladyTest.sol"; +import "./utils/InvariantTest.sol"; + +import {ERC20P, MockERC20P, EIP2612} from "./utils/mocks/MockERC20P.sol"; + +contract ERC20PTest is SoladyTest { + MockERC20P token; + + bytes32 constant PERMIT_TYPEHASH = keccak256( + "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" + ); + + event Transfer(address indexed from, address indexed to, uint256 amount); + + event Approval(address indexed owner, address indexed spender, uint256 amount); + + struct _TestTemps { + address owner; + address to; + uint128 amount; + uint256 deadline; + uint8 v; + bytes32 r; + bytes32 s; + uint256 privateKey; + uint256 nonce; + } + + function _testTemps() internal returns (_TestTemps memory t) { + (t.owner, t.privateKey) = _randomSigner(); + t.to = _randomNonZeroAddress(); + t.amount = uint128(_random()); + t.deadline = _random(); + } + + function setUp() public { + token = new MockERC20P("Token", "TKN", 18); + } + + function testMetadata() public { + assertEq(token.name(), "Token"); + assertEq(token.symbol(), "TKN"); + assertEq(token.decimals(), 18); + } + + function testMint() public { + vm.expectEmit(true, true, true, true); + emit Transfer(address(0), address(0xBEEF), 1e18); + token.mint(address(0xBEEF), 1e18); + + assertEq(token.totalSupply(), 1e18); + assertEq(token.balanceOf(address(0xBEEF)), 1e18); + } + + function testBurn() public { + token.mint(address(0xBEEF), 1e18); + + vm.expectEmit(true, true, true, true); + emit Transfer(address(0xBEEF), address(0), 0.9e18); + token.burn(address(0xBEEF), 0.9e18); + + assertEq(token.totalSupply(), 1e18 - 0.9e18); + assertEq(token.balanceOf(address(0xBEEF)), 0.1e18); + } + + function testApprove() public { + vm.expectEmit(true, true, true, true); + emit Approval(address(this), address(0xBEEF), 1e18); + assertTrue(token.approve(address(0xBEEF), 1e18)); + + assertEq(token.allowance(address(this), address(0xBEEF)), 1e18); + } + + function testTransfer() public { + token.mint(address(this), 1e18); + + vm.expectEmit(true, true, true, true); + emit Transfer(address(this), address(0xBEEF), 1e18); + assertTrue(token.transfer(address(0xBEEF), 1e18)); + assertEq(token.totalSupply(), 1e18); + + assertEq(token.balanceOf(address(this)), 0); + assertEq(token.balanceOf(address(0xBEEF)), 1e18); + } + + function testTransferFrom() public { + address from = address(0xABCD); + + token.mint(from, 1e18); + + vm.prank(from); + token.approve(address(this), 1e18); + + vm.expectEmit(true, true, true, true); + emit Transfer(from, address(0xBEEF), 1e18); + assertTrue(token.transferFrom(from, address(0xBEEF), 1e18)); + assertEq(token.totalSupply(), 1e18); + + assertEq(token.allowance(from, address(this)), 0); + + assertEq(token.balanceOf(from), 0); + assertEq(token.balanceOf(address(0xBEEF)), 1e18); + } + + function testInfiniteApproveTransferFrom() public { + address from = address(0xABCD); + + token.mint(from, 1e18); + + vm.prank(from); + token.approve(address(this), type(uint256).max); + + assertTrue(token.transferFrom(from, address(0xBEEF), 1e18)); + assertEq(token.totalSupply(), 1e18); + + assertEq(token.allowance(from, address(this)), type(uint256).max); + + assertEq(token.balanceOf(from), 0); + assertEq(token.balanceOf(address(0xBEEF)), 1e18); + } + + function testPermit() public { + _TestTemps memory t = _testTemps(); + t.deadline = block.timestamp; + + _signPermit(t); + + _expectPermitEmitApproval(t); + _permit(t); + + _checkAllowanceAndNonce(t); + } + + function testMintOverMaxUintReverts() public { + token.mint(address(this), type(uint256).max); + vm.expectRevert(ERC20P.TotalSupplyOverflow.selector); + token.mint(address(this), 1); + } + + function testTransferInsufficientBalanceReverts() public { + token.mint(address(this), 0.9e18); + vm.expectRevert(ERC20P.InsufficientBalance.selector); + token.transfer(address(0xBEEF), 1e18); + } + + function testTransferFromInsufficientAllowanceReverts() public { + address from = address(0xABCD); + + token.mint(from, 1e18); + + vm.prank(from); + token.approve(address(this), 0.9e18); + + vm.expectRevert(ERC20P.InsufficientAllowance.selector); + token.transferFrom(from, address(0xBEEF), 1e18); + } + + function testTransferFromInsufficientBalanceReverts() public { + address from = address(0xABCD); + + token.mint(from, 0.9e18); + + vm.prank(from); + token.approve(address(this), 1e18); + + vm.expectRevert(ERC20P.InsufficientBalance.selector); + token.transferFrom(from, address(0xBEEF), 1e18); + } + + function testMint(address to, uint128 amount) public { + vm.expectEmit(true, true, true, true); + emit Transfer(address(0), to, amount); + token.mint(to, amount); + + assertEq(token.totalSupply(), amount); + assertEq(token.balanceOf(to), amount); + } + + function testBurn(address from, uint256 mintAmount, uint256 burnAmount) public { + mintAmount = _bound(mintAmount, 0, 2^128); + burnAmount = _bound(burnAmount, 0, mintAmount); + + token.mint(from, mintAmount); + vm.expectEmit(true, true, true, true); + emit Transfer(from, address(0), burnAmount); + token.burn(from, burnAmount); + + assertEq(token.totalSupply(), mintAmount - burnAmount); + assertEq(token.balanceOf(from), mintAmount - burnAmount); + } + + function testApprove(address to, uint256 amount) public { + assertTrue(token.approve(to, amount)); + + assertEq(token.allowance(address(this), to), amount); + } + + function testTransfer(address to, uint128 amount) public { + token.mint(address(this), amount); + + vm.expectEmit(true, true, true, true); + emit Transfer(address(this), to, amount); + assertTrue(token.transfer(to, amount)); + assertEq(token.totalSupply(), amount); + + if (address(this) == to) { + assertEq(token.balanceOf(address(this)), amount); + } else { + assertEq(token.balanceOf(address(this)), 0); + assertEq(token.balanceOf(to), amount); + } + } + + function testTransferFrom( + address spender, + address from, + address to, + uint128 approval, + uint128 amount + ) public { + amount = uint128(_bound(amount, 0, uint256(approval))); + + token.mint(from, amount); + assertEq(token.balanceOf(from), amount); + + vm.prank(from); + token.approve(spender, approval); + + vm.expectEmit(true, true, true, true); + emit Transfer(from, to, amount); + vm.prank(spender); + assertTrue(token.transferFrom(from, to, amount)); + assertEq(token.totalSupply(), amount); + + if (approval == type(uint256).max) { + assertEq(token.allowance(from, spender), approval); + } else { + assertEq(token.allowance(from, spender), approval - amount); + } + + if (from == to) { + assertEq(token.balanceOf(from), amount); + } else { + assertEq(token.balanceOf(from), 0); + assertEq(token.balanceOf(to), amount); + } + } + + function testDirectTransfer(uint256) public { + _TestTemps memory t = _testTemps(); + while (t.owner == t.to) (t.to,) = _randomSigner(); + + uint128 totalSupply = uint128(_random()); + token.mint(t.owner, totalSupply); + assertEq(token.balanceOf(t.owner), totalSupply); + assertEq(token.balanceOf(t.to), 0); + if (t.amount > totalSupply) { + vm.expectRevert(ERC20P.InsufficientBalance.selector); + token.directTransfer(t.owner, t.to, t.amount); + } else { + vm.expectEmit(true, true, true, true); + emit Transfer(t.owner, t.to, t.amount); + token.directTransfer(t.owner, t.to, t.amount); + assertEq(token.balanceOf(t.owner), totalSupply - t.amount); + assertEq(token.balanceOf(t.to), t.amount); + } + } + + function testDirectSpendAllowance(uint256) public { + _TestTemps memory t = _testTemps(); + uint256 allowance = _random(); + vm.prank(t.owner); + token.approve(t.to, allowance); + assertEq(token.allowance(t.owner, t.to), allowance); + if (allowance == type(uint256).max) { + token.directSpendAllowance(t.owner, t.to, t.amount); + assertEq(token.allowance(t.owner, t.to), allowance); + } else if (t.amount > allowance) { + vm.expectRevert(ERC20P.InsufficientAllowance.selector); + token.directSpendAllowance(t.owner, t.to, t.amount); + } else { + token.directSpendAllowance(t.owner, t.to, t.amount); + assertEq(token.allowance(t.owner, t.to), allowance - t.amount); + } + } + + function testPermit(uint256) public { + _TestTemps memory t = _testTemps(); + if (t.deadline < block.timestamp) t.deadline = block.timestamp; + + _signPermit(t); + + _expectPermitEmitApproval(t); + _permit(t); + + _checkAllowanceAndNonce(t); + } + + function _checkAllowanceAndNonce(_TestTemps memory t) internal { + assertEq(token.allowance(t.owner, t.to), t.amount); + assertEq(token.nonces(t.owner), t.nonce + 1); + } + + function testBurnInsufficientBalanceReverts(address to, uint128 mintAmount, uint256 burnAmount) + public + { + if (mintAmount == type(uint128).max) mintAmount--; + burnAmount = _bound(burnAmount, uint256(mintAmount) + 1, type(uint128).max); + + token.mint(to, mintAmount); + vm.expectRevert(ERC20P.InsufficientBalance.selector); + token.burn(to, burnAmount); + } + + function testTransferInsufficientBalanceReverts( + address to, + uint128 mintAmount, + uint128 sendAmount + ) public { + if (mintAmount == type(uint128).max) mintAmount--; + sendAmount = uint128(_bound(sendAmount, mintAmount + 1, type(uint128).max)); + + token.mint(address(this), mintAmount); + vm.expectRevert(ERC20P.InsufficientBalance.selector); + token.transfer(to, sendAmount); + } + + function testTransferFromInsufficientAllowanceReverts( + address to, + uint256 approval, + uint256 amount + ) public { + if (approval == type(uint256).max) approval--; + amount = _bound(amount, approval + 1, type(uint256).max); + + address from = address(0xABCD); + + token.mint(from, amount); + + vm.prank(from); + token.approve(address(this), approval); + + vm.expectRevert(ERC20P.InsufficientAllowance.selector); + token.transferFrom(from, to, amount); + } + + function testTransferFromInsufficientBalanceReverts( + address to, + uint128 mintAmount, + uint128 sendAmount + ) public { + if (mintAmount == type(uint128).max) mintAmount--; + sendAmount = uint128(_bound(sendAmount, mintAmount + 1, type(uint128).max)); + + address from = address(0xABCD); + + token.mint(from, mintAmount); + + vm.prank(from); + token.approve(address(this), sendAmount); + + vm.expectRevert(ERC20P.InsufficientBalance.selector); + token.transferFrom(from, to, sendAmount); + } + + function testPermitBadNonceReverts(uint256) public { + _TestTemps memory t = _testTemps(); + if (t.deadline < block.timestamp) t.deadline = block.timestamp; + while (t.nonce == 0) t.nonce = _random(); + + _signPermit(t); + + vm.expectRevert(EIP2612.InvalidPermit.selector); + _permit(t); + } + + function testPermitBadDeadlineReverts(uint256) public { + _TestTemps memory t = _testTemps(); + if (t.deadline == type(uint256).max) t.deadline--; + if (t.deadline < block.timestamp) t.deadline = block.timestamp; + + _signPermit(t); + + vm.expectRevert(EIP2612.InvalidPermit.selector); + t.deadline += 1; + _permit(t); + } + + function testPermitPastDeadlineReverts(uint256) public { + _TestTemps memory t = _testTemps(); + t.deadline = _bound(t.deadline, 0, block.timestamp - 1); + + _signPermit(t); + + vm.expectRevert(EIP2612.PermitExpired.selector); + _permit(t); + } + + function testPermitReplayReverts(uint256) public { + _TestTemps memory t = _testTemps(); + if (t.deadline < block.timestamp) t.deadline = block.timestamp; + + _signPermit(t); + + _expectPermitEmitApproval(t); + _permit(t); + vm.expectRevert(EIP2612.InvalidPermit.selector); + _permit(t); + } + + function _signPermit(_TestTemps memory t) internal view { + bytes32 innerHash = + keccak256(abi.encode(PERMIT_TYPEHASH, t.owner, t.to, t.amount, t.nonce, t.deadline)); + bytes32 domainSeparator = token.DOMAIN_SEPARATOR(); + bytes32 outerHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, innerHash)); + (t.v, t.r, t.s) = vm.sign(t.privateKey, outerHash); + } + + function _expectPermitEmitApproval(_TestTemps memory t) internal { + vm.expectEmit(true, true, true, true); + emit Approval(t.owner, t.to, t.amount); + } + + function _permit(_TestTemps memory t) internal { + address token_ = address(token); + /// @solidity memory-safe-assembly + assembly { + let m := mload(sub(t, 0x20)) + mstore(sub(t, 0x20), 0xd505accf) + let success := call(gas(), token_, 0, sub(t, 0x04), 0xe4, 0x00, 0x00) + if iszero(success) { + returndatacopy(0, 0, returndatasize()) + revert(0, returndatasize()) + } + mstore(sub(t, 0x20), m) + } + } + + function testBalanceExtraData(uint128 data) public { + address from = address(0xABCD); + + token.setBalanceExtraData(from, data); + + assertEq(token.balanceExtraData(from), data); + } + + function testSetExtraData(uint128 mintAmount, uint128 data) public { + address from = address(0xABCD); + + token.mint(from, mintAmount); + + assertEq(token.balanceOf(from), mintAmount); + + token.setBalanceExtraData(from, data); + + assertEq(token.balanceOf(from), mintAmount); + assertEq(token.balanceExtraData(from), data); + } +} + +contract ERC20PInvariants is SoladyTest, InvariantTest { + BalanceSum balanceSum; + MockERC20P token; + + function setUp() public { + token = new MockERC20P("Token", "TKN", 18); + balanceSum = new BalanceSum(token); + _addTargetContract(address(balanceSum)); + } + + function invariantBalanceSum() public { + assertEq(token.totalSupply(), balanceSum.sum()); + } +} + +contract BalanceSum { + MockERC20P token; + uint256 public sum; + + constructor(MockERC20P _token) { + token = _token; + } + + function mint(address from, uint256 amount) public { + token.mint(from, amount); + sum += amount; + } + + function burn(address from, uint256 amount) public { + token.burn(from, amount); + sum -= amount; + } + + function approve(address to, uint256 amount) public { + token.approve(to, amount); + } + + function transferFrom(address from, address to, uint256 amount) public { + token.transferFrom(from, to, amount); + } + + function transfer(address to, uint256 amount) public { + token.transfer(to, amount); + } +} diff --git a/test/utils/mocks/MockERC20P.sol b/test/utils/mocks/MockERC20P.sol new file mode 100644 index 000000000..7bb33b464 --- /dev/null +++ b/test/utils/mocks/MockERC20P.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import {ERC20P} from "../../../src/tokens/ERC20P/ERC20P.sol"; +import {EIP2612} from "../../../src/tokens/ERC20P/EIP2612.sol"; +import {Brutalizer} from "../Brutalizer.sol"; + +/// @dev WARNING! This mock is strictly intended for testing purposes only. +/// Do NOT copy anything here into production code unless you really know what you are doing. +contract MockERC20P is ERC20P, EIP2612, Brutalizer { + string internal _name; + string internal _symbol; + uint8 internal _decimals; + bytes32 internal immutable _nameHash; + + constructor(string memory name_, string memory symbol_, uint8 decimals_) { + _name = name_; + _symbol = symbol_; + _decimals = decimals_; + _nameHash = keccak256(bytes(name_)); + } + + function _constantNameHash() internal view virtual override returns (bytes32) { + return _nameHash; + } + + function name() public view virtual override(ERC20P, EIP2612) returns (string memory) { + return _name; + } + + function symbol() public view virtual override returns (string memory) { + return _symbol; + } + + function decimals() public view virtual override returns (uint8) { + return _decimals; + } + + function mint(address to, uint256 value) public virtual { + _mint(_brutalized(to), value); + } + + function burn(address from, uint256 value) public virtual { + _burn(_brutalized(from), value); + } + + function directTransfer(address from, address to, uint256 amount) public virtual { + _transfer(_brutalized(from), _brutalized(to), amount); + } + + function directSpendAllowance(address owner, address spender, uint256 amount) public virtual { + _spendAllowance(_brutalized(owner), _brutalized(spender), amount); + } + + function transfer(address to, uint256 amount) public virtual override returns (bool) { + return super.transfer(_brutalized(to), amount); + } + + function transferFrom(address from, address to, uint256 amount) + public + virtual + override + returns (bool) + { + return super.transferFrom(_brutalized(from), _brutalized(to), amount); + } + + function balanceExtraData( + address owner + ) public view returns (uint128 data) { + return _balanceExtraData(owner); + } + + function setBalanceExtraData(address owner, uint128 data) external { + _setBalanceExtraData(owner, data); + } +}