From c5992ca3322c5495a9e9c17c48cf2529cefd1786 Mon Sep 17 00:00:00 2001 From: ross <92001561+z0r0z@users.noreply.github.com> Date: Mon, 17 Feb 2025 12:49:48 +0000 Subject: [PATCH] z01 --- .gitmodules | 6 - LICENSE | 2 +- README.md | 47 ++- foundry.toml | 13 +- lib/forge-std | 1 - lib/solbase | 1 - package-lock.json | 13 - package.json | 13 - src/ERC173.sol | 25 ++ src/ERC20.sol | 62 ++++ src/ReentrancyGuard.sol | 25 ++ src/gov/ERC1155Votes.sol | 181 ----------- src/utils/Receiver.sol | 64 ---- src/utils/ReentrancyGuard.sol | 36 --- src/utils/Refunded.sol | 50 --- src/utils/SafeTransfer.sol | 136 --------- test/ERC173.t.sol | 72 +++++ test/ERC20.t.sol | 348 +++++++++++++++++++++ test/ReentrancyGuard.t.sol | 65 ++-- test/Refunded.t.sol | 32 -- test/SafeTransfer.t.sol | 486 ------------------------------ test/utils/mocks/MockERC173.sol | 26 ++ test/utils/mocks/MockERC20.sol | 29 ++ test/utils/mocks/MockRefunded.sol | 27 -- 24 files changed, 663 insertions(+), 1097 deletions(-) delete mode 160000 lib/forge-std delete mode 160000 lib/solbase delete mode 100644 package-lock.json delete mode 100644 package.json create mode 100644 src/ERC173.sol create mode 100644 src/ERC20.sol create mode 100644 src/ReentrancyGuard.sol delete mode 100644 src/gov/ERC1155Votes.sol delete mode 100644 src/utils/Receiver.sol delete mode 100644 src/utils/ReentrancyGuard.sol delete mode 100644 src/utils/Refunded.sol delete mode 100644 src/utils/SafeTransfer.sol create mode 100644 test/ERC173.t.sol create mode 100644 test/ERC20.t.sol delete mode 100644 test/Refunded.t.sol delete mode 100644 test/SafeTransfer.t.sol create mode 100644 test/utils/mocks/MockERC173.sol create mode 100644 test/utils/mocks/MockERC20.sol delete mode 100644 test/utils/mocks/MockRefunded.sol diff --git a/.gitmodules b/.gitmodules index 9611522..e69de29 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +0,0 @@ -[submodule "lib/forge-std"] - path = lib/forge-std - url = https://github.com/foundry-rs/forge-std -[submodule "lib/solbase"] - path = lib/solbase - url = https://github.com/Sol-DAO/solbase diff --git a/LICENSE b/LICENSE index caee66b..54d3d6f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022, 2023 z0r0z. +Copyright (c) 2022-2025 Zolidity. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 5acd655..93b7ba0 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,45 @@ -# Zolidity -Zero-fuss smart contracts. +# [zolidity](https://github.com/z0r0z/zolidity) [![License: MIT](https://img.shields.io/badge/License-MIT-black.svg)](https://opensource.org/license/mit) [![solidity](https://img.shields.io/badge/solidity-%5E0.8.28-black)](https://docs.soliditylang.org/en/v0.8.28/) [![Foundry](https://img.shields.io/badge/Built%20with-Foundry-000000.svg)](https://getfoundry.sh/) ![tests](https://github.com/z0r0z/zzz/actions/workflows/ci.yml/badge.svg) -Experiments on Ethereum. +`Zolidity`: Zero-to-One Solidity with Simplicity-first. + +## Getting Started + +Run: `curl -L https://foundry.paradigm.xyz | bash && source ~/.bashrc && foundryup` + +Build the foundry project with `forge build`. Run tests with `forge test`. Measure gas with `forge snapshot`. Format with `forge fmt`. + +## GitHub Actions + +Contracts will be tested and gas measured on every push and pull request. + +You can edit the CI script in [.github/workflows/ci.yml](./.github/workflows/ci.yml). + +## Blueprint + +```txt +lib +├─ forge-std — https://github.com/foundry-rs/forge-std +├─ solady — https://github.com/vectorized/solady +src +├─ ERC20 — Standard fungible token. +├─ ERC173 — Standard contract ownership. +├─ ReentrancyGuard — Reentrant call guard. +test +├─ ERC20.t - Test standard fungible token. +├─ ERC173.t — Test standard contract ownership. +└─ ReentrancyGuard.t — Test reentrant call guard. +``` + +## Inspiration + +- [solady](https://github.com/Vectorized/solady) +- [solmate](https://github.com/transmissions11/solmate) +- [snekmate](https://github.com/pcaversaccio/snekmate) + +## Disclaimer + +*These smart contracts and testing suite are being provided as is. No guarantee, representation or warranty is being made, express or implied, as to the safety or correctness of anything provided herein or through related user interfaces. This repository and related code have not been audited and as such there can be no assurance anything will work as intended, and users may experience delays, failures, errors, omissions, loss of transmitted information or loss of funds. The creators are not liable for any of the foregoing. Users should proceed with caution and use at their own risk.* + +## License + +See [LICENSE](./LICENSE) for more details. diff --git a/foundry.toml b/foundry.toml index 1dd47d7..3379d28 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,16 +1,17 @@ [profile.default] -solc = "0.8.19" +solc_version = "0.8.28" +evm_version = "cancun" + optimizer = true optimizer_runs = 9_999_999 -# Configure remappings remappings = [ - "@std=lib/forge-std/src/", - "@solbase=lib/solbase/" + "@solady=lib/solady/", + "@forge=lib/forge-std/src/" ] -[profile.intense.fuzz] -runs = 5_000 +[profile.via-ir] +via_ir = true [fmt] line_length = 100 \ No newline at end of file diff --git a/lib/forge-std b/lib/forge-std deleted file mode 160000 index 8b092b9..0000000 --- a/lib/forge-std +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 8b092b9f2c0fbdcf261939f651ec3c9dcd04538b diff --git a/lib/solbase b/lib/solbase deleted file mode 160000 index e12d00b..0000000 --- a/lib/solbase +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e12d00b9ff196667e3199d61971907510948583b diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 62d8bf4..0000000 --- a/package-lock.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "zolidity", - "version": "0.0.9", - "lockfileVersion": 1, - "requires": true, - "packages": { - "": { - "name": "zolidity", - "version": "0.0.9", - "license": "MIT" - } - } - } \ No newline at end of file diff --git a/package.json b/package.json deleted file mode 100644 index 9302521..0000000 --- a/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "zolidity", - "license": "MIT", - "version": "0.0.9", - "description": "Zero-fuss smart contracts. Experiments on Ethereum.", - "files": [ - "src/**/*.sol" - ], - "repository": { - "type": "git", - "url": "git+https://github.com/z0r0z/zolidity.git" - } -} \ No newline at end of file diff --git a/src/ERC173.sol b/src/ERC173.sol new file mode 100644 index 0000000..26e211c --- /dev/null +++ b/src/ERC173.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +/// @notice Standard contract ownership. +/// @author Zolidity (https://github.com/z0r0z/zolidity/blob/main/src/ERC173.sol) +abstract contract ERC173 { + event OwnershipTransferred(address indexed from, address indexed to); + + error Unauthorized(); + + address public owner; + + modifier onlyOwner() virtual { + require(msg.sender == owner, Unauthorized()); + _; + } + + function _setOwner(address to) internal virtual { + emit OwnershipTransferred(msg.sender, owner = to); + } + + function transferOwnership(address to) public virtual onlyOwner { + _setOwner(to); + } +} diff --git a/src/ERC20.sol b/src/ERC20.sol new file mode 100644 index 0000000..b0b4cdd --- /dev/null +++ b/src/ERC20.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +/// @notice Standard fungible token. +/// @author Zolidity (https://github.com/z0r0z/zolidity/blob/main/src/ERC20.sol) +abstract contract ERC20 { + event Approval(address indexed from, address indexed to, uint256 amount); + event Transfer(address indexed from, address indexed to, uint256 amount); + + string public name; + string public symbol; + uint256 public constant decimals = 18; + + uint256 public totalSupply; + + mapping(address holder => uint256) public balanceOf; + mapping(address holder => mapping(address spender => uint256)) public allowance; + + constructor(string memory _name, string memory _symbol) payable { + (name, symbol) = (_name, _symbol); + } + + function approve(address to, uint256 amount) public virtual returns (bool) { + allowance[msg.sender][to] = amount; + emit Approval(msg.sender, to, amount); + return true; + } + + function transfer(address to, uint256 amount) public virtual returns (bool) { + return transferFrom(msg.sender, to, amount); + } + + function transferFrom(address from, address to, uint256 amount) public virtual returns (bool) { + if (from != msg.sender) { + if (allowance[from][msg.sender] != type(uint256).max) { + allowance[from][msg.sender] -= amount; + } + } + balanceOf[from] -= amount; + unchecked { + balanceOf[to] += amount; + } + emit Transfer(from, to, amount); + return true; + } + + function _mint(address to, uint256 amount) internal virtual { + totalSupply += amount; + unchecked { + balanceOf[to] += amount; + } + emit Transfer(address(0), to, amount); + } + + function _burn(address from, uint256 amount) internal virtual { + balanceOf[from] -= amount; + unchecked { + totalSupply -= amount; + } + emit Transfer(from, address(0), amount); + } +} diff --git a/src/ReentrancyGuard.sol b/src/ReentrancyGuard.sol new file mode 100644 index 0000000..86c2d36 --- /dev/null +++ b/src/ReentrancyGuard.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +/// @notice Reentrant call guard. +/// @author Zolidity (https://github.com/z0r0z/zolidity/blob/main/src/ReentrancyGuard.sol) +abstract contract ReentrancyGuard { + error Reentrancy(); + + uint256 internal _guard = 1; + + modifier nonReentrant() virtual { + _setReentrancyGuard(); + _; + _clearReentrancyGuard(); + } + + function _setReentrancyGuard() internal virtual { + require(_guard != 2, Reentrancy()); + _guard = 2; + } + + function _clearReentrancyGuard() internal virtual { + _guard = 1; + } +} diff --git a/src/gov/ERC1155Votes.sol b/src/gov/ERC1155Votes.sol deleted file mode 100644 index 4b43e44..0000000 --- a/src/gov/ERC1155Votes.sol +++ /dev/null @@ -1,181 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.4; - -/// @dev Contracts. -import {ERC1155} from "@solbase/src/tokens/ERC1155/ERC1155.sol"; - -/// @dev Libraries. -import {SafeCastLib} from "@solbase/src/utils/SafeCastLib.sol"; - -/// @notice Compound-like voting extension for ERC1155. -/// @author z0r0z.eth -abstract contract ERC1155Votes is ERC1155 { - /// ----------------------------------------------------------------------- - /// Library Usage - /// ----------------------------------------------------------------------- - - using SafeCastLib for uint256; - - /// ----------------------------------------------------------------------- - /// Events - /// ----------------------------------------------------------------------- - - event DelegateChanged( - address indexed delegator, - address indexed fromDelegate, - address indexed toDelegate, - uint256 id - ); - - event DelegateVotesChanged( - address indexed delegate, uint256 indexed id, uint256 previousBalance, uint256 newBalance - ); - - /// ----------------------------------------------------------------------- - /// Checkpoint Storage - /// ----------------------------------------------------------------------- - - mapping(address => mapping(uint256 => address)) internal _delegates; - - mapping(address => mapping(uint256 => uint256)) public numCheckpoints; - - mapping(address => mapping(uint256 => mapping(uint256 => Checkpoint))) public checkpoints; - - struct Checkpoint { - uint64 fromTimestamp; - uint192 votes; - } - - /// ----------------------------------------------------------------------- - /// Delegation Logic - /// ----------------------------------------------------------------------- - - function delegates(address account, uint256 id) public view virtual returns (address) { - address current = _delegates[account][id]; - - return current == address(0) ? account : current; - } - - function getCurrentVotes(address account, uint256 id) public view virtual returns (uint256) { - // Won't underflow because decrement only occurs if positive `nCheckpoints`. - unchecked { - uint256 nCheckpoints = numCheckpoints[account][id]; - - return nCheckpoints != 0 ? checkpoints[account][id][nCheckpoints - 1].votes : 0; - } - } - - function getPriorVotes(address account, uint256 id, uint256 timestamp) - public - view - virtual - returns (uint256) - { - require(block.timestamp > timestamp, "UNDETERMINED"); - - uint256 nCheckpoints = numCheckpoints[account][id]; - - if (nCheckpoints == 0) { - return 0; - } - - // Won't underflow because decrement only occurs if positive `nCheckpoints`. - unchecked { - // First check most recent balance. - if (checkpoints[account][id][nCheckpoints - 1].fromTimestamp <= timestamp) { - return checkpoints[account][id][nCheckpoints - 1].votes; - } - - // Next check implicit zero balance. - if (checkpoints[account][id][0].fromTimestamp > timestamp) { - return 0; - } - - uint256 lower; - - uint256 upper = nCheckpoints - 1; - - while (upper > lower) { - uint256 center = upper - (upper - lower) / 2; - - Checkpoint memory cp = checkpoints[account][id][center]; - - if (cp.fromTimestamp == timestamp) { - return cp.votes; - } else if (cp.fromTimestamp < timestamp) { - lower = center; - } else { - upper = center - 1; - } - } - - return checkpoints[account][id][lower].votes; - } - } - - function delegate(address delegatee, uint256 id) public virtual { - address currentDelegate = delegates(msg.sender, id); - - _delegates[msg.sender][id] = delegatee; - - emit DelegateChanged(msg.sender, currentDelegate, delegatee, id); - - _moveDelegates(currentDelegate, delegatee, id, balanceOf[msg.sender][id]); - } - - function _moveDelegates(address srcRep, address dstRep, uint256 id, uint256 amount) - internal - virtual - { - if (srcRep != dstRep && amount != 0) { - if (srcRep != address(0)) { - uint256 srcRepNum = numCheckpoints[srcRep][id]; - - uint256 srcRepOld = - srcRepNum != 0 ? checkpoints[srcRep][id][srcRepNum - 1].votes : 0; - - _writeCheckpoint(srcRep, id, srcRepNum, srcRepOld, srcRepOld - amount); - } - - if (dstRep != address(0)) { - uint256 dstRepNum = numCheckpoints[dstRep][id]; - - uint256 dstRepOld = - dstRepNum != 0 ? checkpoints[dstRep][id][dstRepNum - 1].votes : 0; - - _writeCheckpoint(dstRep, id, dstRepNum, dstRepOld, dstRepOld + amount); - } - } - } - - function _writeCheckpoint( - address delegatee, - uint256 id, - uint256 nCheckpoints, - uint256 oldVotes, - uint256 newVotes - ) - internal - virtual - { - unchecked { - uint64 timestamp = block.timestamp.safeCastTo64(); - - // Won't underflow because decrement only occurs if positive `nCheckpoints`. - if ( - nCheckpoints != 0 - && checkpoints[delegatee][id][nCheckpoints - 1].fromTimestamp == timestamp - ) { - checkpoints[delegatee][id][nCheckpoints - 1].votes = newVotes.safeCastTo192(); - } else { - checkpoints[delegatee][id][nCheckpoints] = - Checkpoint(timestamp, newVotes.safeCastTo192()); - - // Won't realistically overflow. - ++numCheckpoints[delegatee][id]; - } - } - - emit DelegateVotesChanged(delegatee, id, oldVotes, newVotes); - } -} diff --git a/src/utils/Receiver.sol b/src/utils/Receiver.sol deleted file mode 100644 index 170bdd4..0000000 --- a/src/utils/Receiver.sol +++ /dev/null @@ -1,64 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.4; - -/// @notice ETH, ERC721 and ERC1155 receiver logic. -contract Receiver { - /// ----------------------------------------------------------------------- - /// ERC165 Introspection - /// ----------------------------------------------------------------------- - - function supportsInterface( - bytes4 interfaceId - ) public view virtual returns (bool) { - return - // ERC165 interface ID for ERC165. - interfaceId == this.supportsInterface.selector || - // ERC165 Interface ID for ERC721TokenReceiver. - interfaceId == this.onERC721Received.selector || - // ERC165 Interface ID for ERC1155TokenReceiver. - interfaceId == 0x4e2312e0; - } - - /// ----------------------------------------------------------------------- - /// ETH Receiver - /// ----------------------------------------------------------------------- - - receive() external payable {} - - /// ----------------------------------------------------------------------- - /// ERC721 Receiver - /// ----------------------------------------------------------------------- - - function onERC721Received( - address, - address, - uint256, - bytes calldata - ) public payable virtual returns (bytes4) { - return this.onERC721Received.selector; - } - - /// ----------------------------------------------------------------------- - /// ERC1155 Receiver - /// ----------------------------------------------------------------------- - - function onERC1155Received( - address, - address, - uint256, - uint256, - bytes calldata - ) public payable virtual returns (bytes4) { - return this.onERC1155Received.selector; - } - - function onERC1155BatchReceived( - address, - address, - uint256[] calldata, - uint256[] calldata, - bytes calldata - ) public payable virtual returns (bytes4) { - return this.onERC1155BatchReceived.selector; - } -} diff --git a/src/utils/ReentrancyGuard.sol b/src/utils/ReentrancyGuard.sol deleted file mode 100644 index 87bde77..0000000 --- a/src/utils/ReentrancyGuard.sol +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.4; - -/// @notice Reentrancy protection for smart contracts. -/// @author z0r0z.eth -/// @author Modified from Seaport -/// (https://github.com/ProjectOpenSea/seaport/blob/main/contracts/lib/ReentrancyGuard.sol) -/// @author Modified from Solmate -/// (https://github.com/Rari-Capital/solmate/blob/main/src/utils/ReentrancyGuard.sol) -abstract contract ReentrancyGuard { - error Reentrancy(); - - uint256 private guard = 1; - - modifier nonReentrant() virtual { - setReentrancyGuard(); - - _; - - clearReentrancyGuard(); - } - - /// @dev Check guard sentinel value and set it. - function setReentrancyGuard() internal virtual { - if (guard == 2) { - revert Reentrancy(); - } - - guard = 2; - } - - /// @dev Unset sentinel value. - function clearReentrancyGuard() internal virtual { - guard = 1; - } -} diff --git a/src/utils/Refunded.sol b/src/utils/Refunded.sol deleted file mode 100644 index efeda11..0000000 --- a/src/utils/Refunded.sol +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.4; - -import {ReentrancyGuard} from "./ReentrancyGuard.sol"; -import {safeTransferETH} from "./SafeTransfer.sol"; - -/// @notice Gas refunds for smart contracts. -/// @author z0r0z.eth -/// @custom:coauthor saucepoint -/// @dev Gas should be deposited for refunds. -abstract contract Refunded is ReentrancyGuard { - error GasMax(address emitter); - - uint256 internal constant BASE_COST = 25433; - - uint256 internal constant GAS_PRICE_MAX = 4e10; - - constructor() payable {} - - receive() external payable virtual {} - - /// @dev Modified functions over 21k gas - /// benefit most from refund. - modifier isRefunded() virtual { - // Memo starting gas. - uint256 refund = gasleft(); - - setReentrancyGuard(); - - // Check malicious refund. - unchecked { - if (tx.gasprice > block.basefee + GAS_PRICE_MAX) { - revert GasMax(address(this)); - } - } - - _; - - // Memo ending gas. - // (BASE_COST + (refund - gasleft())) * tx.gasprice - assembly { - refund := mul(add(BASE_COST, sub(refund, gas())), gasprice()) - } - - // Refund gas fee. - safeTransferETH(tx.origin, refund); - - clearReentrancyGuard(); - } -} diff --git a/src/utils/SafeTransfer.sol b/src/utils/SafeTransfer.sol deleted file mode 100644 index f131b18..0000000 --- a/src/utils/SafeTransfer.sol +++ /dev/null @@ -1,136 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.4; - -/// @notice Safe ETH and ERC20 free function transfer collection that gracefully handles missing return values. -/// @author Zolidity (https://github.com/z0r0z/zolidity/blob/main/src/utils/SafeTransfer.sol) -/// @author Modified from Solady (https://github.com/vectorized/solady/blob/main/src/utils/SafeTransferLib.sol) - -/// @dev The ETH transfer has failed. -error ETHTransferFailed(); - -/// @dev Sends `amount` (in wei) ETH to `to`. -/// Reverts upon failure. -function safeTransferETH(address to, uint256 amount) { - assembly { - // Transfer the ETH and check if it succeeded or not. - if iszero(call(gas(), to, amount, 0, 0, 0, 0)) { - // Store the function selector of `ETHTransferFailed()`. - mstore(0x00, 0xb12d13eb) - // Revert with (offset, size). - revert(0x1c, 0x04) - } - } -} - -/// @dev The ERC20 `approve` has failed. -error ApproveFailed(); - -/// @dev Sets `amount` of ERC20 `token` for `to` to manage on behalf of the current contract. -/// Reverts upon failure. -function safeApprove(address token, address to, uint256 amount) { - assembly { - // We'll write our calldata to this slot below, but restore it later. - let memPointer := mload(0x40) - - // Write the abi-encoded calldata into memory, beginning with the function selector. - mstore(0x00, 0x095ea7b3) - mstore(0x20, to) // Append the "to" argument. - mstore(0x40, amount) // Append the "amount" argument. - - if iszero( - and( - // Set success to whether the call reverted, if not we check it either - // returned exactly 1 (can't just be non-zero data), or had no return data. - or(eq(mload(0x00), 1), iszero(returndatasize())), - // We use 0x44 because that's the total length of our calldata (0x04 + 0x20 * 2) - // Counterintuitively, this call() must be positioned after the or() in the - // surrounding and() because and() evaluates its arguments from right to left. - call(gas(), token, 0, 0x1c, 0x44, 0x00, 0x20) - ) - ) { - // Store the function selector of `ApproveFailed()`. - mstore(0x00, 0x3e3f8f73) - // Revert with (offset, size). - revert(0x1c, 0x04) - } - - mstore(0x40, memPointer) // Restore the memPointer. - } -} - -/// @dev The ERC20 `transfer` has failed. -error TransferFailed(); - -/// @dev Sends `amount` of ERC20 `token` from the current contract to `to`. -/// Reverts upon failure. -function safeTransfer(address token, address to, uint256 amount) { - assembly { - // We'll write our calldata to this slot below, but restore it later. - let memPointer := mload(0x40) - - // Write the abi-encoded calldata into memory, beginning with the function selector. - mstore(0x00, 0xa9059cbb) - mstore(0x20, to) // Append the "to" argument. - mstore(0x40, amount) // Append the "amount" argument. - - if iszero( - and( - // Set success to whether the call reverted, if not we check it either - // returned exactly 1 (can't just be non-zero data), or had no return data. - or(eq(mload(0x00), 1), iszero(returndatasize())), - // We use 0x44 because that's the total length of our calldata (0x04 + 0x20 * 2) - // Counterintuitively, this call() must be positioned after the or() in the - // surrounding and() because and() evaluates its arguments from right to left. - call(gas(), token, 0, 0x1c, 0x44, 0x00, 0x20) - ) - ) { - // Store the function selector of `TransferFailed()`. - mstore(0x00, 0x90b8ec18) - // Revert with (offset, size). - revert(0x1c, 0x04) - } - - mstore(0x40, memPointer) // Restore the memPointer. - } -} - -/// @dev The ERC20 `transferFrom` has failed. -error TransferFromFailed(); - -/// @dev Sends `amount` of ERC20 `token` from `from` to `to`. -/// Reverts upon failure. -/// -/// The `from` account must have at least `amount` approved for -/// the current contract to manage. -function safeTransferFrom(address token, address from, address to, uint256 amount) { - assembly { - // We'll write our calldata to this slot below, but restore it later. - let memPointer := mload(0x40) - - // Write the abi-encoded calldata into memory, beginning with the function selector. - mstore(0x00, 0x23b872dd) - mstore(0x20, from) // Append the "from" argument. - mstore(0x40, to) // Append the "to" argument. - mstore(0x60, amount) // Append the "amount" argument. - - if iszero( - and( - // Set success to whether the call reverted, if not we check it either - // returned exactly 1 (can't just be non-zero data), or had no return data. - or(eq(mload(0x00), 1), iszero(returndatasize())), - // We use 0x64 because that's the total length of our calldata (0x04 + 0x20 * 3) - // Counterintuitively, this call() must be positioned after the or() in the - // surrounding and() because and() evaluates its arguments from right to left. - call(gas(), token, 0, 0x1c, 0x64, 0x00, 0x20) - ) - ) { - // Store the function selector of `TransferFromFailed()`. - mstore(0x00, 0x7939f424) - // Revert with (offset, size). - revert(0x1c, 0x04) - } - - mstore(0x60, 0) // Restore the zero slot to zero. - mstore(0x40, memPointer) // Restore the memPointer. - } -} diff --git a/test/ERC173.t.sol b/test/ERC173.t.sol new file mode 100644 index 0000000..4376b9f --- /dev/null +++ b/test/ERC173.t.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {SoladyTest} from "@solady/test/utils/SoladyTest.sol"; +import {ERC173, MockERC173} from "./utils/mocks/MockERC173.sol"; + +/// @dev Adapted from Ownable.t.sol (https://github.com/Vectorized/solady/blob/main/test/Ownable.t.sol) +/// - The only changes are the following as they are not used in Zolidity ERC173: +/// - - Remove initialization tests (`_setOwner` is recycled throughout ERC173) +/// - - Remove Handover tests (no two-step pattern in ERC173 specification) +contract ERC173Test is SoladyTest { + event OwnershipTransferred(address indexed from, address indexed to); + + MockERC173 mockERC173; + + function setUp() public { + mockERC173 = new MockERC173(); + } + + function testSetOwnerDirect(address newOwner) public { + vm.expectEmit(true, true, true, true); + emit OwnershipTransferred(address(this), _cleaned(newOwner)); + mockERC173.setOwnerDirect(newOwner); + assertEq(mockERC173.owner(), newOwner); + } + + function testSetOwnerDirect() public { + testSetOwnerDirect(address(1)); + } + + function testTransferOwnership( + address newOwner, + bool setNewOwnerToZeroAddress, + bool callerIsOwner + ) public { + assertEq(mockERC173.owner(), address(this)); + + while (newOwner == address(this)) newOwner = _randomNonZeroAddress(); + + if (newOwner == address(0) || setNewOwnerToZeroAddress) { + newOwner = address(0); + vm.expectEmit(true, true, true, true); + emit OwnershipTransferred(address(this), _cleaned(newOwner)); + } else if (callerIsOwner) { + vm.expectEmit(true, true, true, true); + emit OwnershipTransferred(address(this), _cleaned(newOwner)); + } else { + vm.prank(newOwner); + vm.expectRevert(ERC173.Unauthorized.selector); + } + + mockERC173.transferOwnership(newOwner); + + if (newOwner != address(0) && callerIsOwner) { + assertEq(mockERC173.owner(), newOwner); + } + } + + function testTransferOwnership() public { + testTransferOwnership(address(1), false, true); + } + + function testOnlyOwnerModifier(address nonOwner, bool callerIsOwner) public { + while (nonOwner == address(this)) nonOwner = _randomNonZeroAddress(); + + if (!callerIsOwner) { + vm.prank(nonOwner); + vm.expectRevert(ERC173.Unauthorized.selector); + } + mockERC173.updateFlagWithOnlyOwner(); + } +} diff --git a/test/ERC20.t.sol b/test/ERC20.t.sol new file mode 100644 index 0000000..f829a72 --- /dev/null +++ b/test/ERC20.t.sol @@ -0,0 +1,348 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {MockERC20} from "./utils/mocks/MockERC20.sol"; +import {InvariantTest} from "@solady/test/utils/InvariantTest.sol"; +import {stdError, SoladyTest} from "@solady/test/utils/SoladyTest.sol"; + +/// @dev Adapted from ERC20.t.sol (https://github.com/Vectorized/solady/blob/main/test/ERC20.t.sol) +/// - The only changes are the following as they are not used in Zolidity ERC20: +/// - - Remove Permit tests +/// - - Remove direct transfer/approve tests +contract ERC20Test is SoladyTest { + event Transfer(address indexed from, address indexed to, uint256 amount); + event Approval(address indexed owner, address indexed spender, uint256 amount); + + MockERC20 token; + + struct _TestTemps { + address owner; + address to; + uint256 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 = _random(); + t.deadline = _random(); + } + + function setUp() public { + token = new MockERC20(); + } + + function testMetadata() public { + assertEq(token.name(), "TEST"); + assertEq(token.symbol(), "TEST"); + 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 testMintOverMaxUintReverts() public { + token.mint(address(this), type(uint256).max); + vm.expectRevert(stdError.arithmeticError); + token.mint(address(this), 1); + } + + function testTransferInsufficientBalanceReverts() public { + token.mint(address(this), 0.9e18); + vm.expectRevert(stdError.arithmeticError); + 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(stdError.arithmeticError); + 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(stdError.arithmeticError); + token.transferFrom(from, address(0xBEEF), 1e18); + } + + function testMint(address to, uint256 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 { + 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, uint256 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, + uint256 approval, + uint256 amount + ) public { + vm.assume(spender != from); + amount = _bound(amount, 0, 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 _checkAllowanceAndNonce(_TestTemps memory t) internal { + assertEq(token.allowance(t.owner, t.to), t.amount); + } + + function testBurnInsufficientBalanceReverts(address to, uint256 mintAmount, uint256 burnAmount) + public + { + if (mintAmount == type(uint256).max) mintAmount--; + burnAmount = _bound(burnAmount, mintAmount + 1, type(uint256).max); + + token.mint(to, mintAmount); + vm.expectRevert(stdError.arithmeticError); + token.burn(to, burnAmount); + } + + function testTransferInsufficientBalanceReverts( + address to, + uint256 mintAmount, + uint256 sendAmount + ) public { + if (mintAmount == type(uint256).max) mintAmount--; + sendAmount = _bound(sendAmount, mintAmount + 1, type(uint256).max); + + token.mint(address(this), mintAmount); + vm.expectRevert(stdError.arithmeticError); + 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(stdError.arithmeticError); + token.transferFrom(from, to, amount); + } + + function testTransferFromInsufficientBalanceReverts( + address to, + uint256 mintAmount, + uint256 sendAmount + ) public { + if (mintAmount == type(uint256).max) mintAmount--; + sendAmount = _bound(sendAmount, mintAmount + 1, type(uint256).max); + + address from = address(0xABCD); + + token.mint(from, mintAmount); + + vm.prank(from); + token.approve(address(this), sendAmount); + + vm.expectRevert(stdError.arithmeticError); + token.transferFrom(from, to, sendAmount); + } +} + +contract ERC20Invariants is SoladyTest, InvariantTest { + BalanceSum balanceSum; + MockERC20 token; + + function setUp() public { + token = new MockERC20(); + balanceSum = new BalanceSum(token); + _addTargetContract(address(balanceSum)); + } + + function invariantBalanceSum() public { + assertEq(token.totalSupply(), balanceSum.sum()); + } +} + +contract BalanceSum { + MockERC20 token; + uint256 public sum; + + constructor(MockERC20 _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/ReentrancyGuard.t.sol b/test/ReentrancyGuard.t.sol index 44e4c92..1cb3fbb 100644 --- a/test/ReentrancyGuard.t.sol +++ b/test/ReentrancyGuard.t.sol @@ -1,60 +1,43 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.4; +pragma solidity ^0.8.19; -import {DSTestPlus} from "@solbase/test/utils/DSTestPlus.sol"; +import {Test} from "@forge/Test.sol"; +import {ReentrancyGuard} from "../src/ReentrancyGuard.sol"; -import {ReentrancyGuard} from "../src/utils/ReentrancyGuard.sol"; +contract MockReentrant is ReentrancyGuard { + uint256 public counter; -contract RiskyContract is ReentrancyGuard { - uint256 public enterTimes; - - function unprotectedCall() public { - enterTimes++; - - if (enterTimes > 1) { - return; - } - - this.protectedCall(); + function protectedFunction() external nonReentrant { + counter++; + if (counter == 1) this.protectedFunction(); } - function protectedCall() public nonReentrant { - enterTimes++; - - if (enterTimes > 1) { - return; - } - - this.protectedCall(); + function unprotectedFunction() external { + counter++; + if (counter == 1) this.unprotectedFunction(); } - - function overprotectedCall() public nonReentrant {} } -contract ReentrancyGuardTest is DSTestPlus { - RiskyContract riskyContract; +contract ReentrancyGuardTest is Test { + MockReentrant mock; function setUp() public { - riskyContract = new RiskyContract(); - } - - function invariantReentrancyStatusAlways1() public { - assertEq(uint256(hevm.load(address(riskyContract), 0)), 1); + mock = new MockReentrant(); } - function testFailUnprotectedCall() public { - riskyContract.unprotectedCall(); - - assertEq(riskyContract.enterTimes(), 1); + function testProtectedFunctionPreventsReentrancy() public { + vm.expectRevert(ReentrancyGuard.Reentrancy.selector); + mock.protectedFunction(); } - function testProtectedCall() public { - try riskyContract.protectedCall() { - fail("Reentrancy Guard Failed To Stop Attacker"); - } catch {} + function testUnprotectedFunctionAllowsReentrancy() public { + mock.unprotectedFunction(); + assertEq(mock.counter(), 2); } - function testNoReentrancy() public { - riskyContract.overprotectedCall(); + function testGuardValueReset() public { + try mock.protectedFunction() {} catch {} + uint256 slot0 = uint256(vm.load(address(mock), bytes32(0))); + assertEq(slot0, 1, "Guard should be reset to 1"); } } diff --git a/test/Refunded.t.sol b/test/Refunded.t.sol deleted file mode 100644 index 338cb38..0000000 --- a/test/Refunded.t.sol +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity >=0.8.4; - -import {MockRefunded} from "./utils/mocks/MockRefunded.sol"; - -import "@std/Test.sol"; - -contract RefundedTest is Test { - using stdStorage for StdStorage; - - MockRefunded mock; - - function setUp() public { - console.log(unicode"🧪 Testing Refunded..."); - - mock = new MockRefunded(); - } - - // VM Cheatcodes can be found in ./lib/forge-std/src/Vm.sol - // Or at https://github.com/foundry-rs/forge-std - function testDeploy() public { - new MockRefunded(); - } - - function testRefunded() public { - mock.foo(); - } - - function testUnrefunded() public { - mock.bar(); - } -} diff --git a/test/SafeTransfer.t.sol b/test/SafeTransfer.t.sol deleted file mode 100644 index c581465..0000000 --- a/test/SafeTransfer.t.sol +++ /dev/null @@ -1,486 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.4; - -import {MockERC20} from "@solbase/test/utils/mocks/MockERC20.sol"; -import {RevertingToken} from "@solbase/test/utils/weird-tokens/RevertingToken.sol"; -import {ReturnsTwoToken} from "@solbase/test/utils/weird-tokens/ReturnsTwoToken.sol"; -import {ReturnsFalseToken} from "@solbase/test/utils/weird-tokens/ReturnsFalseToken.sol"; -import {MissingReturnToken} from "@solbase/test/utils/weird-tokens/MissingReturnToken.sol"; -import {ReturnsTooMuchToken} from "@solbase/test/utils/weird-tokens/ReturnsTooMuchToken.sol"; -import {ReturnsGarbageToken} from "@solbase/test/utils/weird-tokens/ReturnsGarbageToken.sol"; -import {ReturnsTooLittleToken} from "@solbase/test/utils/weird-tokens/ReturnsTooLittleToken.sol"; - -import "@solbase/test/utils/TestPlus.sol"; - -import {ERC20} from "@solbase/src/tokens/ERC20/ERC20.sol"; -import { - ETHTransferFailed, - safeTransferETH, - ApproveFailed, - safeApprove, - TransferFailed, - safeTransfer, - TransferFromFailed, - safeTransferFrom -} from "../src/utils/SafeTransfer.sol"; - -contract SafeTransferTest is TestPlus { - RevertingToken reverting; - ReturnsTwoToken returnsTwo; - ReturnsFalseToken returnsFalse; - MissingReturnToken missingReturn; - ReturnsTooMuchToken returnsTooMuch; - ReturnsGarbageToken returnsGarbage; - ReturnsTooLittleToken returnsTooLittle; - - MockERC20 erc20; - - function setUp() public { - reverting = new RevertingToken(); - returnsTwo = new ReturnsTwoToken(); - returnsFalse = new ReturnsFalseToken(); - missingReturn = new MissingReturnToken(); - returnsTooMuch = new ReturnsTooMuchToken(); - returnsGarbage = new ReturnsGarbageToken(); - returnsTooLittle = new ReturnsTooLittleToken(); - - erc20 = new MockERC20("StandardToken", "ST", 18); - erc20.mint(address(this), type(uint256).max); - } - - function testTransferWithMissingReturn() public { - verifySafeTransfer(address(missingReturn), address(0xBEEF), 1e18); - } - - function testTransferWithStandardERC20() public { - verifySafeTransfer(address(erc20), address(0xBEEF), 1e18); - } - - function testTransferWithReturnsTooMuch() public { - verifySafeTransfer(address(returnsTooMuch), address(0xBEEF), 1e18); - } - - function testTransferWithNonContract() public { - safeTransfer(address(0xBADBEEF), address(0xBEEF), 1e18); - } - - function testTransferFromWithMissingReturn() public { - verifySafeTransferFrom(address(missingReturn), address(0xFEED), address(0xBEEF), 1e18); - } - - function testTransferFromWithStandardERC20() public { - verifySafeTransferFrom(address(erc20), address(0xFEED), address(0xBEEF), 1e18); - } - - function testTransferFromWithReturnsTooMuch() public { - verifySafeTransferFrom(address(returnsTooMuch), address(0xFEED), address(0xBEEF), 1e18); - } - - function testTransferFromWithNonContract() public { - safeTransferFrom(address(0xBADBEEF), address(0xFEED), address(0xBEEF), 1e18); - } - - function testApproveWithMissingReturn() public { - verifySafeApprove(address(missingReturn), address(0xBEEF), 1e18); - } - - function testApproveWithStandardERC20() public { - verifySafeApprove(address(erc20), address(0xBEEF), 1e18); - } - - function testApproveWithReturnsTooMuch() public { - verifySafeApprove(address(returnsTooMuch), address(0xBEEF), 1e18); - } - - function testApproveWithNonContract() public { - safeApprove(address(0xBADBEEF), address(0xBEEF), 1e18); - } - - function testTransferETH() public { - safeTransferETH(address(0xBEEF), 1e18); - } - - function testTransferRevertSelector() public { - vm.expectRevert(TransferFailed.selector); - this.testFailTransferWithReturnsFalse(); - } - - function testTransferFromRevertSelector() public { - vm.expectRevert(TransferFromFailed.selector); - this.testFailTransferFromWithReturnsFalse(); - } - - function testApproveRevertSelector() public { - vm.expectRevert(ApproveFailed.selector); - this.testFailApproveWithReturnsFalse(); - } - - function testTransferETHRevertSelector() public { - vm.expectRevert(ETHTransferFailed.selector); - this.testFailTransferETHToContractWithoutFallback(); - } - - function testFailTransferWithReturnsFalse() public { - verifySafeTransfer(address(returnsFalse), address(0xBEEF), 1e18); - } - - function testFailTransferWithReverting() public { - verifySafeTransfer(address(reverting), address(0xBEEF), 1e18); - } - - function testFailTransferWithReturnsTooLittle() public { - verifySafeTransfer(address(returnsTooLittle), address(0xBEEF), 1e18); - } - - function testFailTransferFromWithReturnsFalse() public { - verifySafeTransferFrom(address(returnsFalse), address(0xFEED), address(0xBEEF), 1e18); - } - - function testFailTransferFromWithReverting() public { - verifySafeTransferFrom(address(reverting), address(0xFEED), address(0xBEEF), 1e18); - } - - function testFailTransferFromWithReturnsTooLittle() public { - verifySafeTransferFrom(address(returnsTooLittle), address(0xFEED), address(0xBEEF), 1e18); - } - - function testFailApproveWithReturnsFalse() public { - verifySafeApprove(address(returnsFalse), address(0xBEEF), 1e18); - } - - function testFailApproveWithReverting() public { - verifySafeApprove(address(reverting), address(0xBEEF), 1e18); - } - - function testFailApproveWithReturnsTooLittle() public { - verifySafeApprove(address(returnsTooLittle), address(0xBEEF), 1e18); - } - - function testFuzzTransferWithMissingReturn(address to, uint256 amount) public brutalizeMemory { - verifySafeTransfer(address(missingReturn), to, amount); - } - - function testFuzzTransferWithStandardERC20(address to, uint256 amount) public brutalizeMemory { - verifySafeTransfer(address(erc20), to, amount); - } - - function testFuzzTransferWithReturnsTooMuch(address to, uint256 amount) - public - brutalizeMemory - { - verifySafeTransfer(address(returnsTooMuch), to, amount); - } - - function testFuzzTransferWithGarbage(address to, uint256 amount, bytes memory garbage) - public - brutalizeMemory - { - if (garbageIsGarbage(garbage)) { - return; - } - - returnsGarbage.setGarbage(garbage); - - verifySafeTransfer(address(returnsGarbage), to, amount); - } - - function testFuzzTransferWithNonContract(address nonContract, address to, uint256 amount) - public - brutalizeMemory - { - if (uint256(uint160(nonContract)) <= 18 || nonContract.code.length > 0) { - return; - } - - safeTransfer(nonContract, to, amount); - } - - function testFailTransferETHToContractWithoutFallback() public { - safeTransferETH(address(this), 1e18); - } - - function testFuzzTransferFromWithMissingReturn(address from, address to, uint256 amount) - public - brutalizeMemory - { - verifySafeTransferFrom(address(missingReturn), from, to, amount); - } - - function testFuzzTransferFromWithStandardERC20(address from, address to, uint256 amount) - public - brutalizeMemory - { - verifySafeTransferFrom(address(erc20), from, to, amount); - } - - function testFuzzTransferFromWithReturnsTooMuch(address from, address to, uint256 amount) - public - brutalizeMemory - { - verifySafeTransferFrom(address(returnsTooMuch), from, to, amount); - } - - function testFuzzTransferFromWithGarbage( - address from, - address to, - uint256 amount, - bytes memory garbage - ) - public - brutalizeMemory - { - if (garbageIsGarbage(garbage)) { - return; - } - - returnsGarbage.setGarbage(garbage); - - verifySafeTransferFrom(address(returnsGarbage), from, to, amount); - } - - function testFuzzTransferFromWithNonContract( - address nonContract, - address from, - address to, - uint256 amount - ) - public - brutalizeMemory - { - if (uint256(uint160(nonContract)) <= 18 || nonContract.code.length > 0) { - return; - } - - safeTransferFrom(nonContract, from, to, amount); - } - - function testFuzzApproveWithMissingReturn(address to, uint256 amount) public brutalizeMemory { - verifySafeApprove(address(missingReturn), to, amount); - } - - function testFuzzApproveWithStandardERC20(address to, uint256 amount) public brutalizeMemory { - verifySafeApprove(address(erc20), to, amount); - } - - function testFuzzApproveWithReturnsTooMuch(address to, uint256 amount) public brutalizeMemory { - verifySafeApprove(address(returnsTooMuch), to, amount); - } - - function testFuzzApproveWithGarbage(address to, uint256 amount, bytes memory garbage) - public - brutalizeMemory - { - if (garbageIsGarbage(garbage)) { - return; - } - - returnsGarbage.setGarbage(garbage); - - verifySafeApprove(address(returnsGarbage), to, amount); - } - - function testFuzzApproveWithNonContract(address nonContract, address to, uint256 amount) - public - brutalizeMemory - { - if (uint256(uint160(nonContract)) <= 18 || nonContract.code.length > 0) { - return; - } - - safeApprove(nonContract, to, amount); - } - - function testFuzzTransferETH(address recipient, uint256 amount) public brutalizeMemory { - // Transferring to msg.sender can fail because it's possible to overflow their ETH balance as it begins non-zero. - if ( - recipient.code.length > 0 || uint256(uint160(recipient)) <= 18 - || recipient == msg.sender - ) { - return; - } - - amount = bound(amount, 0, address(this).balance); - - safeTransferETH(recipient, amount); - } - - function testFailFuzzTransferWithReturnsFalse(address to, uint256 amount) - public - brutalizeMemory - { - verifySafeTransfer(address(returnsFalse), to, amount); - } - - function testFailFuzzTransferWithReverting(address to, uint256 amount) public brutalizeMemory { - verifySafeTransfer(address(reverting), to, amount); - } - - function testFailFuzzTransferWithReturnsTooLittle(address to, uint256 amount) - public - brutalizeMemory - { - verifySafeTransfer(address(returnsTooLittle), to, amount); - } - - function testFailFuzzTransferWithReturnsTwo(address to, uint256 amount) - public - brutalizeMemory - { - verifySafeTransfer(address(returnsTwo), to, amount); - } - - function testFailFuzzTransferWithGarbage(address to, uint256 amount, bytes memory garbage) - public - brutalizeMemory - { - require(garbageIsGarbage(garbage)); - - returnsGarbage.setGarbage(garbage); - - verifySafeTransfer(address(returnsGarbage), to, amount); - } - - function testFailFuzzTransferFromWithReturnsFalse(address from, address to, uint256 amount) - public - brutalizeMemory - { - verifySafeTransferFrom(address(returnsFalse), from, to, amount); - } - - function testFailFuzzTransferFromWithReverting(address from, address to, uint256 amount) - public - brutalizeMemory - { - verifySafeTransferFrom(address(reverting), from, to, amount); - } - - function testFailFuzzTransferFromWithReturnsTooLittle(address from, address to, uint256 amount) - public - brutalizeMemory - { - verifySafeTransferFrom(address(returnsTooLittle), from, to, amount); - } - - function testFailFuzzTransferFromWithReturnsTwo(address from, address to, uint256 amount) - public - brutalizeMemory - { - verifySafeTransferFrom(address(returnsTwo), from, to, amount); - } - - function testFailFuzzTransferFromWithGarbage( - address from, - address to, - uint256 amount, - bytes memory garbage - ) - public - brutalizeMemory - { - require(garbageIsGarbage(garbage)); - - returnsGarbage.setGarbage(garbage); - - verifySafeTransferFrom(address(returnsGarbage), from, to, amount); - } - - function testFailFuzzApproveWithReturnsFalse(address to, uint256 amount) - public - brutalizeMemory - { - verifySafeApprove(address(returnsFalse), to, amount); - } - - function testFailFuzzApproveWithReverting(address to, uint256 amount) public brutalizeMemory { - verifySafeApprove(address(reverting), to, amount); - } - - function testFailFuzzApproveWithReturnsTooLittle(address to, uint256 amount) - public - brutalizeMemory - { - verifySafeApprove(address(returnsTooLittle), to, amount); - } - - function testFailFuzzApproveWithReturnsTwo(address to, uint256 amount) public brutalizeMemory { - verifySafeApprove(address(returnsTwo), to, amount); - } - - function testFailFuzzApproveWithGarbage(address to, uint256 amount, bytes memory garbage) - public - brutalizeMemory - { - require(garbageIsGarbage(garbage)); - - returnsGarbage.setGarbage(garbage); - - verifySafeApprove(address(returnsGarbage), to, amount); - } - - function testFailFuzzTransferETHToContractWithoutFallback(uint256 amount) - public - brutalizeMemory - { - safeTransferETH(address(this), amount); - } - - function verifySafeTransfer(address token, address to, uint256 amount) public { - uint256 preBal = ERC20(token).balanceOf(to); - safeTransfer(address(token), to, amount); - uint256 postBal = ERC20(token).balanceOf(to); - - if (to == address(this)) { - assertEq(preBal, postBal); - } else { - assertEq(postBal - preBal, amount); - } - } - - function verifySafeTransferFrom(address token, address from, address to, uint256 amount) - public - { - forceApprove(token, from, address(this), amount); - - // We cast to MissingReturnToken here because it won't check - // that there was return data, which accommodates all tokens. - MissingReturnToken(token).transfer(from, amount); - - uint256 preBal = ERC20(token).balanceOf(to); - safeTransferFrom(token, from, to, amount); - uint256 postBal = ERC20(token).balanceOf(to); - - if (from == to) { - assertEq(preBal, postBal); - } else { - assertEq(postBal - preBal, amount); - } - } - - function verifySafeApprove(address token, address to, uint256 amount) public { - safeApprove(address(token), to, amount); - - assertEq(ERC20(token).allowance(address(this), to), amount); - } - - function forceApprove(address token, address from, address to, uint256 amount) public { - uint256 slot = token == address(erc20) ? 4 : 2; // Standard ERC20 name and symbol aren't constant. - - vm.store( - token, - keccak256(abi.encode(to, keccak256(abi.encode(from, uint256(slot))))), - bytes32(uint256(amount)) - ); - - assertEq(ERC20(token).allowance(from, to), amount, "wrong allowance"); - } - - function garbageIsGarbage(bytes memory garbage) public pure returns (bool result) { - assembly { - result := - and( - or(lt(mload(garbage), 32), iszero(eq(mload(add(garbage, 0x20)), 1))), - gt(mload(garbage), 0) - ) - } - } -} diff --git a/test/utils/mocks/MockERC173.sol b/test/utils/mocks/MockERC173.sol new file mode 100644 index 0000000..37db446 --- /dev/null +++ b/test/utils/mocks/MockERC173.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {ERC173} from "../../../src/ERC173.sol"; +import {Brutalizer} from "@solady/test/utils/Brutalizer.sol"; + +/// @notice Standard test contract ownership. +contract MockERC173 is ERC173, Brutalizer { + bool public flag; + + constructor() payable { + _setOwner(msg.sender); + } + + function setOwnerDirect(address to) public { + _setOwner(_brutalized(to)); + } + + function transferOwnership(address to) public virtual override(ERC173) { + super.transferOwnership(_brutalized(to)); + } + + function updateFlagWithOnlyOwner() public onlyOwner { + flag = true; + } +} diff --git a/test/utils/mocks/MockERC20.sol b/test/utils/mocks/MockERC20.sol new file mode 100644 index 0000000..6f04e63 --- /dev/null +++ b/test/utils/mocks/MockERC20.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {ERC20} from "../../../src/ERC20.sol"; +import {Brutalizer} from "@solady/test/utils/Brutalizer.sol"; + +/// @notice Standard fungible test token. +contract MockERC20 is ERC20("TEST", "TEST"), Brutalizer { + function mint(address to, uint256 amount) public virtual { + _mint(_brutalized(to), amount); + } + + function burn(address from, uint256 amount) public virtual { + _burn(_brutalized(from), amount); + } + + function transfer(address to, uint256 amount) public virtual override(ERC20) returns (bool) { + return super.transfer(_brutalized(to), amount); + } + + function transferFrom(address from, address to, uint256 amount) + public + virtual + override(ERC20) + returns (bool) + { + return super.transferFrom(_brutalized(from), _brutalized(to), amount); + } +} diff --git a/test/utils/mocks/MockRefunded.sol b/test/utils/mocks/MockRefunded.sol deleted file mode 100644 index 2af5a4d..0000000 --- a/test/utils/mocks/MockRefunded.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity >=0.8.4; - -import {Refunded} from "../../../src/utils/Refunded.sol"; - -/// @notice Mock Refunded contract. -contract MockRefunded is Refunded { - uint256 private counter; - - /// @dev Refunded transaction, - /// where *most* of the gas-cost is returned to the caller (tx.origin). - function foo() public isRefunded { - expensive(); - } - - /// @dev Unrefunded transaction, for testing & comparison. - function bar() public { - expensive(); - } - - /// @dev A gas-expensive function. - function expensive() public { - for (uint256 i = 0; i < 50; i++) { - counter++; - } - } -}