diff --git a/.gas-snapshot b/.gas-snapshot index 25f3157a..8d09dbc9 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,42 +1,151 @@ -testAcceptingOwner() (gas: 139707) -testFailNonOwner2() (gas: 3773) -testFailRejectingAuthority1() (gas: 119902) -testFailNonOwner1() (gas: 3742) -testFailRejectingAuthority2() (gas: 119999) +testFailSetAuthorityWithRestrictiveAuthority() (gas: 126002) +testSetAuthorityWithPermissiveAuthority() (gas: 127687) +testFailSetOwnerWithRestrictiveAuthority() (gas: 126166) +testFailCallFunctionAsNonOwner() (gas: 4191) +testSetAuthorityAsOwner() (gas: 23802) +testFailCallFunctionAsOwnerWithOutOfOrderAuthority() (gas: 135733) +testCallFunctionWithPermissiveAuthority() (gas: 125973) +testFailSetAuthorityAsNonOwner() (gas: 6960) +testFailSetOwnerAsOwnerWithOutOfOrderAuthority() (gas: 135873) +testCallFunctionAsOwner() (gas: 21371) +testFailCallFunctionWithRestrictiveAuthority() (gas: 126125) +testSetOwnerWithPermissiveAuthority() (gas: 147508) +testFailSetOwnerAsNonOwner() (gas: 4309) +testSetAuthorityAsOwnerWithOutOfOrderAuthority() (gas: 234329) +testSetOwnerAsOwner() (gas: 3998) testFromLast20Bytes() (gas: 191) testFillLast12Bytes() (gas: 223) -testFailDoubleDeploySameBytecode() (gas: 277076930206519) -testDeployERC20() (gas: 885671) -testFailDoubleDeployDifferentBytecode() (gas: 277076930206511) -testMin() (gas: 715) -testFPow() (gas: 1738) -testMax() (gas: 757) -testFailFDivZeroXY() (gas: 298) -testSqrt() (gas: 2342) -testFDiv() (gas: 764) -testFDivEdgeCases() (gas: 543) -testFMulEdgeCases() (gas: 823) -testFailFDivXYB() (gas: 319) -testFailFDivZeroY() (gas: 274) +testFailDoubleDeploySameBytecode() (gas: 277076930206699) +testDeployERC20() (gas: 873896) +testFailDoubleDeployDifferentBytecode() (gas: 277076930214885) +testFailBoundMinBiggerThanMax() (gas: 309) +testBound() (gas: 5520) +testFailSafeBatchTransferFromToRevertingERC1155Recipient() (gas: 1041163) +testMintToEOA() (gas: 30265) +testFailMintToNonERC155Recipient() (gas: 71897) +testFailSafeBatchTransferFromToZero() (gas: 805864) +testBatchMintToERC1155Recipient() (gas: 946375) +testApproveAll() (gas: 26509) +testFailSafeBatchTransferFromWithArrayLengthMismatch() (gas: 681042) +testFailBatchMintToZero() (gas: 127242) +testFailSafeBatchTransferFromToWrongReturnDataERC1155Recipient() (gas: 993087) +testSafeTransferFromToERC1155Recipient() (gas: 1210543) +testFailBatchMintToWrongReturnDataERC1155Recipient() (gas: 314473) +testFailBatchMintToRevertingERC1155Recipient() (gas: 362536) +testBatchBurn() (gas: 146591) +testFailBurnInsufficientBalance() (gas: 30352) +testFailSafeTransferFromToWrongReturnDataERC1155Recipient() (gas: 243471) +testFailMintToRevertingERC155Recipient() (gas: 263148) +testFailSafeBatchTransferFromToNonERC1155Recipient() (gas: 849621) +testFailSafeTransferFromInsufficientBalance() (gas: 579173) +testFailSafeTransferFromToNonERC155Recipient() (gas: 100376) +testFailBatchMintToNonERC1155Recipient() (gas: 171010) +testSafeBatchTransferFromToEOA() (gas: 817122) +testFailSafeTransferFromToRevertingERC1155Recipient() (gas: 291604) +testBatchMintToEOA() (gas: 132842) +testFailBatchBurnInsufficientBalance() (gas: 131673) +testSafeBatchTransferFromToERC1155Recipient() (gas: 1650504) +testFailBalanceOfBatchWithArrayMismatch() (gas: 4798) +testFailSafeBatchTransferInsufficientBalance() (gas: 682003) +testSafeTransferFromToEOA() (gas: 609087) +testMintToERC1155Recipient() (gas: 612041) +testFailBatchMintWithArrayMismatch() (gas: 5118) +testBatchBalanceOf() (gas: 153798) +testFailSafeTransferFromToZero() (gas: 57667) +testFailSafeTransferFromSelfInsufficientBalance() (gas: 29956) +testBurn() (gas: 34098) +testFailBatchBurnWithArrayLengthMismatch() (gas: 131065) +testFailMintToZero() (gas: 29205) +testSafeTransferFromSelf() (gas: 59828) +testFailMintToWrongReturnDataERC155Recipient() (gas: 263102) +testInfiniteApproveTransferFrom() (gas: 387796) +testApprove() (gas: 26558) +testMetaData() (gas: 6966) +testTransferFrom() (gas: 388134) +testFailTransferFromInsufficientBalance() (gas: 359401) +testFailPermitPastDeadline() (gas: 2197) +testFailPermitReplay() (gas: 59949) +testMint() (gas: 49180) +testFailTransferFromInsufficientAllowance() (gas: 358925) +testTransfer() (gas: 75628) +testBurn() (gas: 52492) +testPermit() (gas: 56782) +testFailTransferInsufficientBalance() (gas: 48240) +testFailPermitBadDeadline() (gas: 30486) +testFailPermitBadNonce() (gas: 30436) +testSafeTransferFromToERC721Recipient() (gas: 908869) +testFailSafeMintToERC721RecipientWithWrongReturnDataWithData() (gas: 185732) +testApprove() (gas: 96031) +testFailBurnUnMinted() (gas: 3379) +testFailSafeTransferFromToERC721RecipientWithWrongReturnDataWithData() (gas: 213867) +testFailDoubleMint() (gas: 70935) +testApproveAll() (gas: 26585) +testFailApproveUnAuthorized() (gas: 73181) +testFailSafeTransferFromToRevertingERC721RecipientWithData() (gas: 259577) +testFailSafeMintToNonERC721RecipientWithData() (gas: 115867) +testMetadata() (gas: 6492) +testFailTransferFromWrongFrom() (gas: 71032) +testFailSafeMintToRevertingERC721Recipient() (gas: 230626) +testTransferFrom() (gas: 551359) +testFailSafeMintToNonERC721Recipient() (gas: 115042) +testFailDoubleBurn() (gas: 74563) +testFailSafeMintToERC721RecipientWithWrongReturnData() (gas: 184893) +testFailSafeTransferFromToNonERC721Recipient() (gas: 143245) +testMint() (gas: 72701) +testFailApproveUnMinted() (gas: 5694) +testFailTransferFromToZero() (gas: 71031) +testSafeMintToERC721Recipient() (gas: 408375) +testSafeTransferFromToEOA() (gas: 556215) +testSafeMintToEOA() (gas: 75400) +testFailSafeTransferFromToERC721RecipientWithWrongReturnData() (gas: 213093) +testTransferFromApproveAll() (gas: 553534) +testFailTransferFromUnOwned() (gas: 3500) +testFailSafeTransferFromToNonERC721RecipientWithData() (gas: 144048) +testBurn() (gas: 76417) +testFailSafeMintToRevertingERC721RecipientWithData() (gas: 231396) +testFailMintToZero() (gas: 1253) +testFailTransferFromNotOwner() (gas: 75544) +testSafeMintToERC721RecipientWithData() (gas: 429537) +testFailSafeTransferFromToRevertingERC721Recipient() (gas: 258848) +testSafeTransferFromToERC721RecipientWithData() (gas: 930031) +testTransferFromSelf() (gas: 103082) +testFPow() (gas: 1651) +testFailFDivZeroXY() (gas: 316) +testSqrt() (gas: 2492) +testFDiv() (gas: 733) +testFDivEdgeCases() (gas: 581) +testFMulEdgeCases() (gas: 801) +testFailFDivXYB() (gas: 294) +testFailFDivZeroY() (gas: 271) testFMul() (gas: 669) +testSetRoles() (gas: 33023) +testCanCallWithCustomAuthorityOverridesPublicCapability() (gas: 295417) +testCanCallPublicCapability() (gas: 39631) +testSetTargetCustomAuthority() (gas: 31736) +testCanCallWithCustomAuthorityOverridesUserWithRole() (gas: 334265) +testCanCallWithAuthorizedRole() (gas: 97461) +testSetRoleCapabilities() (gas: 32997) +testCanCallWithCustomAuthority() (gas: 466959) +testSetPublicCapabilities() (gas: 31468) testNoReentrancy() (gas: 1015) testProtectedCall() (gas: 23649) testFailUnprotectedCall() (gas: 30515) -testBasics() (gas: 76765) -testRoot() (gas: 40181) -testSanityChecks() (gas: 11630) -testPublicCapabilities() (gas: 41708) -testWriteRead() (gas: 53564) -testWriteReadFullStartBound() (gas: 34778) -testFailWriteReadEmptyOutOfBounds() (gas: 34479) -testWriteReadFullBoundedRead() (gas: 53761) +testSetRoles() (gas: 32998) +testCanCallPublicCapability() (gas: 38436) +testCanCallWithAuthorizedRole() (gas: 96267) +testSetRoleCapabilities() (gas: 34588) +testSetPublicCapabilities() (gas: 33244) +testWriteRead() (gas: 53511) +testWriteReadFullStartBound() (gas: 34725) +testFailWriteReadEmptyOutOfBounds() (gas: 34432) +testWriteReadFullBoundedRead() (gas: 53708) testFailReadInvalidPointer() (gas: 2905) -testFailWriteReadOutOfStartBound() (gas: 34393) +testFailWriteReadOutOfStartBound() (gas: 34346) testFailReadInvalidPointerCustomStartBound() (gas: 2982) -testWriteReadEmptyBound() (gas: 34692) -testFailWriteReadOutOfBounds() (gas: 34500) -testWriteReadCustomBounds() (gas: 34906) -testWriteReadCustomStartBound() (gas: 34821) +testWriteReadEmptyBound() (gas: 34639) +testFailWriteReadOutOfBounds() (gas: 34453) +testWriteReadCustomBounds() (gas: 34853) +testWriteReadCustomStartBound() (gas: 34768) testFailReadInvalidPointerCustomBounds() (gas: 3143) testSafeCastTo248() (gas: 433) testSafeCastTo128() (gas: 455) @@ -68,8 +177,6 @@ testTransferWithNonContract() (gas: 3075) testApproveWithTransferFromSelf() (gas: 26416) testTransferWithTransferFromSelf() (gas: 28182) testFailTransferETHToContractWithoutFallback() (gas: 7222) -testUpdateTrust() (gas: 12713) -testSanityChecks() (gas: 4838) testPartialWithdraw() (gas: 68803) testDeposit() (gas: 58804) testFallbackDeposit() (gas: 59068) diff --git a/.gitignore b/.gitignore index 88cb1153..5dfe93fb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ -/out -/node_modules \ No newline at end of file +/cache +/node_modules +/out \ No newline at end of file diff --git a/README.md b/README.md index cbefd300..1e3cf76c 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,14 @@ ```ml auth ├─ Auth — "Flexible and updatable auth pattern" -├─ Trust — "Ultra minimal authorization logic" ├─ authorities │ ├─ RolesAuthority — "Role based Authority that supports up to 256 roles" -│ ├─ TrustAuthority — "Simple Authority which only authorizes trusted users" +│ ├─ MultiRolesAuthority — "Flexible and target agnostic role based Authority" tokens ├─ WETH — "Minimalist and modern Wrapped Ether implementation" ├─ ERC20 — "Modern and gas efficient ERC20 + EIP-2612 implementation" +├─ ERC721 — "Modern, minimalist, and gas efficient ERC721 implementation" +├─ ERC1155 — "Minimalist and gas efficient standard ERC1155 implementation" utils ├─ SSTORE2 - "Library for cheaper reads and writes to persistent storage" ├─ CREATE3 — "Deploy to deterministic addresses without an initcode factor" diff --git a/audits/v6-Fixed-Point-Solutions.pdf b/audits/v6-Fixed-Point-Solutions.pdf new file mode 100644 index 00000000..5c424342 Binary files /dev/null and b/audits/v6-Fixed-Point-Solutions.pdf differ diff --git a/src/auth/Auth.sol b/src/auth/Auth.sol index fe5c7078..2cf75592 100644 --- a/src/auth/Auth.sol +++ b/src/auth/Auth.sol @@ -1,22 +1,13 @@ // SPDX-License-Identifier: AGPL-3.0-only -pragma solidity >=0.7.0; - -/// @notice A generic interface for a contract which provides authorization data to an Auth instance. -/// @author Modified from Dappsys (https://github.com/dapphub/ds-auth/blob/master/src/auth.sol) -interface Authority { - function canCall( - address user, - address target, - bytes4 functionSig - ) external view returns (bool); -} +pragma solidity >=0.8.0; /// @notice Provides a flexible and updatable auth pattern which is completely separate from application logic. +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/auth/Auth.sol) /// @author Modified from Dappsys (https://github.com/dapphub/ds-auth/blob/master/src/auth.sol) abstract contract Auth { - event OwnerUpdated(address indexed owner); + event OwnerUpdated(address indexed user, address indexed newOwner); - event AuthorityUpdated(Authority indexed authority); + event AuthorityUpdated(address indexed user, Authority indexed newAuthority); address public owner; @@ -26,37 +17,48 @@ abstract contract Auth { owner = _owner; authority = _authority; - emit OwnerUpdated(_owner); - emit AuthorityUpdated(_authority); + emit OwnerUpdated(msg.sender, _owner); + emit AuthorityUpdated(msg.sender, _authority); } - function setOwner(address newOwner) public virtual requiresAuth { - owner = newOwner; + modifier requiresAuth() { + require(isAuthorized(msg.sender, msg.sig), "UNAUTHORIZED"); - emit OwnerUpdated(owner); + _; } - function setAuthority(Authority newAuthority) public virtual requiresAuth { - authority = newAuthority; + function isAuthorized(address user, bytes4 functionSig) internal view virtual returns (bool) { + Authority auth = authority; // Memoizing authority saves us a warm SLOAD, around 100 gas. - emit AuthorityUpdated(authority); + // Checking if the caller is the owner only after calling the authority saves gas in most cases, but be + // aware that this makes protected functions uncallable even to the owner if the authority is out of order. + return (address(auth) != address(0) && auth.canCall(user, address(this), functionSig)) || user == owner; } - function isAuthorized(address user, bytes4 functionSig) internal view virtual returns (bool) { - Authority cachedAuthority = authority; + function setAuthority(Authority newAuthority) public virtual { + // We check if the caller is the owner first because we want to ensure they can + // always swap out the authority even if it's reverting or using up a lot of gas. + require(msg.sender == owner || authority.canCall(msg.sender, address(this), msg.sig)); - if (address(cachedAuthority) != address(0)) { - try cachedAuthority.canCall(user, address(this), functionSig) returns (bool canCall) { - if (canCall) return true; - } catch {} - } + authority = newAuthority; - return user == owner; + emit AuthorityUpdated(msg.sender, newAuthority); } - modifier requiresAuth() { - require(isAuthorized(msg.sender, msg.sig), "UNAUTHORIZED"); + function setOwner(address newOwner) public virtual requiresAuth { + owner = newOwner; - _; + emit OwnerUpdated(msg.sender, newOwner); } } + +/// @notice A generic interface for a contract which provides authorization data to an Auth instance. +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/auth/Auth.sol) +/// @author Modified from Dappsys (https://github.com/dapphub/ds-auth/blob/master/src/auth.sol) +interface Authority { + function canCall( + address user, + address target, + bytes4 functionSig + ) external view returns (bool); +} diff --git a/src/auth/Trust.sol b/src/auth/Trust.sol deleted file mode 100644 index 76b072cd..00000000 --- a/src/auth/Trust.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity >=0.7.0; - -/// @notice Ultra minimal authorization logic for smart contracts. -/// @author Inspired by Dappsys V2 (https://github.com/dapp-org/dappsys-v2/blob/main/src/auth.sol) -abstract contract Trust { - event UserTrustUpdated(address indexed user, bool trusted); - - mapping(address => bool) public isTrusted; - - constructor(address initialUser) { - isTrusted[initialUser] = true; - - emit UserTrustUpdated(initialUser, true); - } - - function setIsTrusted(address user, bool trusted) public virtual requiresTrust { - isTrusted[user] = trusted; - - emit UserTrustUpdated(user, trusted); - } - - modifier requiresTrust() { - require(isTrusted[msg.sender], "UNTRUSTED"); - - _; - } -} diff --git a/src/auth/authorities/MultiRolesAuthority.sol b/src/auth/authorities/MultiRolesAuthority.sol new file mode 100644 index 00000000..3329714c --- /dev/null +++ b/src/auth/authorities/MultiRolesAuthority.sol @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.10; + +import {Auth, Authority} from "../Auth.sol"; + +/// @notice Flexible and target agnostic role based Authority that supports up to 256 roles. +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/auth/authorities/MultiRolesAuthority.sol) +contract MultiRolesAuthority is Auth, Authority { + /*/////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + event UserRoleUpdated(address indexed user, uint8 indexed role, bool enabled); + + event PublicCapabilityUpdated(bytes4 indexed functionSig, bool enabled); + + event RoleCapabilityUpdated(uint8 indexed role, bytes4 indexed functionSig, bool enabled); + + event TargetCustomAuthorityUpdated(address indexed target, Authority indexed authority); + + /*/////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + constructor(address _owner, Authority _authority) Auth(_owner, _authority) {} + + /*/////////////////////////////////////////////////////////////// + CUSTOM TARGET AUTHORITY STORAGE + //////////////////////////////////////////////////////////////*/ + + mapping(address => Authority) public getTargetCustomAuthority; + + /*/////////////////////////////////////////////////////////////// + ROLE/USER STORAGE + //////////////////////////////////////////////////////////////*/ + + mapping(address => bytes32) public getUserRoles; + + mapping(bytes4 => bool) public isCapabilityPublic; + + mapping(bytes4 => bytes32) public getRolesWithCapability; + + function doesUserHaveRole(address user, uint8 role) public view virtual returns (bool) { + return (uint256(getUserRoles[user]) >> role) & 1 != 0; + } + + function doesRoleHaveCapability(uint8 role, bytes4 functionSig) public view virtual returns (bool) { + return (uint256(getRolesWithCapability[functionSig]) >> role) & 1 != 0; + } + + /*/////////////////////////////////////////////////////////////// + AUTHORIZATION LOGIC + //////////////////////////////////////////////////////////////*/ + + function canCall( + address user, + address target, + bytes4 functionSig + ) public view virtual override returns (bool) { + Authority customAuthority = getTargetCustomAuthority[target]; + + if (address(customAuthority) != address(0)) return customAuthority.canCall(user, target, functionSig); + + return + isCapabilityPublic[functionSig] || bytes32(0) != getUserRoles[user] & getRolesWithCapability[functionSig]; + } + + /*/////////////////////////////////////////////////////////////// + CUSTOM TARGET AUTHORITY CONFIGURATION LOGIC + //////////////////////////////////////////////////////////////*/ + + function setTargetCustomAuthority(address target, Authority customAuthority) public virtual requiresAuth { + getTargetCustomAuthority[target] = customAuthority; + + emit TargetCustomAuthorityUpdated(target, customAuthority); + } + + /*/////////////////////////////////////////////////////////////// + PUBLIC CAPABILITY CONFIGURATION LOGIC + //////////////////////////////////////////////////////////////*/ + + function setPublicCapability(bytes4 functionSig, bool enabled) public virtual requiresAuth { + isCapabilityPublic[functionSig] = enabled; + + emit PublicCapabilityUpdated(functionSig, enabled); + } + + /*/////////////////////////////////////////////////////////////// + USER ROLE ASSIGNMENT LOGIC + //////////////////////////////////////////////////////////////*/ + + function setUserRole( + address user, + uint8 role, + bool enabled + ) public virtual requiresAuth { + if (enabled) { + getUserRoles[user] |= bytes32(1 << role); + } else { + getUserRoles[user] &= ~bytes32(1 << role); + } + + emit UserRoleUpdated(user, role, enabled); + } + + /*/////////////////////////////////////////////////////////////// + ROLE CAPABILITY CONFIGURATION LOGIC + //////////////////////////////////////////////////////////////*/ + + function setRoleCapability( + uint8 role, + bytes4 functionSig, + bool enabled + ) public virtual requiresAuth { + if (enabled) { + getRolesWithCapability[functionSig] |= bytes32(1 << role); + } else { + getRolesWithCapability[functionSig] &= ~bytes32(1 << role); + } + + emit RoleCapabilityUpdated(role, functionSig, enabled); + } +} diff --git a/src/auth/authorities/RolesAuthority.sol b/src/auth/authorities/RolesAuthority.sol index ef5c89a2..94e394f6 100644 --- a/src/auth/authorities/RolesAuthority.sol +++ b/src/auth/authorities/RolesAuthority.sol @@ -4,14 +4,13 @@ pragma solidity >=0.8.0; import {Auth, Authority} from "../Auth.sol"; /// @notice Role based Authority that supports up to 256 roles. +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/auth/authorities/RolesAuthority.sol) /// @author Modified from Dappsys (https://github.com/dapphub/ds-roles/blob/master/src/roles.sol) contract RolesAuthority is Auth, Authority { /*/////////////////////////////////////////////////////////////// EVENTS //////////////////////////////////////////////////////////////*/ - event UserRootUpdated(address indexed user, bool enabled); - event UserRoleUpdated(address indexed user, uint8 indexed role, bool enabled); event PublicCapabilityUpdated(address indexed target, bytes4 indexed functionSig, bool enabled); @@ -21,42 +20,29 @@ contract RolesAuthority is Auth, Authority { /*/////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + constructor(address _owner, Authority _authority) Auth(_owner, _authority) {} /*/////////////////////////////////////////////////////////////// - USER ROLE STORAGE + ROLE/USER STORAGE //////////////////////////////////////////////////////////////*/ - mapping(address => bool) public isUserRoot; - mapping(address => bytes32) public getUserRoles; - function doesUserHaveRole(address user, uint8 role) public view virtual returns (bool) { - unchecked { - bytes32 shifted = bytes32(uint256(uint256(2)**uint256(role))); - - return bytes32(0) != getUserRoles[user] & shifted; - } - } - - /*/////////////////////////////////////////////////////////////// - ROLE CAPABILITY STORAGE - //////////////////////////////////////////////////////////////*/ + mapping(address => mapping(bytes4 => bool)) public isCapabilityPublic; - mapping(address => mapping(bytes4 => bytes32)) public getRoleCapabilities; + mapping(address => mapping(bytes4 => bytes32)) public getRolesWithCapability; - mapping(address => mapping(bytes4 => bool)) public isCapabilityPublic; + function doesUserHaveRole(address user, uint8 role) public view virtual returns (bool) { + return (uint256(getUserRoles[user]) >> role) & 1 != 0; + } function doesRoleHaveCapability( uint8 role, address target, bytes4 functionSig ) public view virtual returns (bool) { - unchecked { - bytes32 shifted = bytes32(uint256(uint256(2)**uint256(role))); - - return bytes32(0) != getRoleCapabilities[target][functionSig] & shifted; - } + return (uint256(getRolesWithCapability[target][functionSig]) >> role) & 1 != 0; } /*/////////////////////////////////////////////////////////////// @@ -68,9 +54,9 @@ contract RolesAuthority is Auth, Authority { address target, bytes4 functionSig ) public view virtual override returns (bool) { - if (isCapabilityPublic[target][functionSig]) return true; - - return bytes32(0) != getUserRoles[user] & getRoleCapabilities[target][functionSig] || isUserRoot[user]; + return + isCapabilityPublic[target][functionSig] || + bytes32(0) != getUserRoles[user] & getRolesWithCapability[target][functionSig]; } /*/////////////////////////////////////////////////////////////// @@ -93,14 +79,10 @@ contract RolesAuthority is Auth, Authority { bytes4 functionSig, bool enabled ) public virtual requiresAuth { - bytes32 lastCapabilities = getRoleCapabilities[target][functionSig]; - - unchecked { - bytes32 shifted = bytes32(uint256(uint256(2)**uint256(role))); - - getRoleCapabilities[target][functionSig] = enabled - ? lastCapabilities | shifted - : lastCapabilities & ~shifted; + if (enabled) { + getRolesWithCapability[target][functionSig] |= bytes32(1 << role); + } else { + getRolesWithCapability[target][functionSig] &= ~bytes32(1 << role); } emit RoleCapabilityUpdated(role, target, functionSig, enabled); @@ -115,20 +97,12 @@ contract RolesAuthority is Auth, Authority { uint8 role, bool enabled ) public virtual requiresAuth { - bytes32 lastRoles = getUserRoles[user]; - - unchecked { - bytes32 shifted = bytes32(uint256(uint256(2)**uint256(role))); - - getUserRoles[user] = enabled ? lastRoles | shifted : lastRoles & ~shifted; + if (enabled) { + getUserRoles[user] |= bytes32(1 << role); + } else { + getUserRoles[user] &= ~bytes32(1 << role); } emit UserRoleUpdated(user, role, enabled); } - - function setRootUser(address user, bool enabled) public virtual requiresAuth { - isUserRoot[user] = enabled; - - emit UserRootUpdated(user, enabled); - } } diff --git a/src/auth/authorities/TrustAuthority.sol b/src/auth/authorities/TrustAuthority.sol deleted file mode 100644 index ad203b20..00000000 --- a/src/auth/authorities/TrustAuthority.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity >=0.7.0; - -import {Trust} from "../Trust.sol"; -import {Authority} from "../Auth.sol"; - -/// @notice Simple Authority that allows a Trust to be used as an Authority. -/// @author Original work by Transmissions11 (https://github.com/transmissions11) -contract TrustAuthority is Trust, Authority { - constructor(address initialUser) Trust(initialUser) {} - - function canCall( - address user, - address, - bytes4 - ) public view virtual override returns (bool) { - return isTrusted[user]; - } -} diff --git a/src/test/Auth.t.sol b/src/test/Auth.t.sol index 91709c41..5ac743bc 100644 --- a/src/test/Auth.t.sol +++ b/src/test/Auth.t.sol @@ -3,22 +3,17 @@ pragma solidity 0.8.10; import {DSTestPlus} from "./utils/DSTestPlus.sol"; import {MockAuthChild} from "./utils/mocks/MockAuthChild.sol"; +import {MockAuthority} from "./utils/mocks/MockAuthority.sol"; -import {Auth, Authority} from "../auth/Auth.sol"; - -contract BooleanAuthority is Authority { - bool yes; - - constructor(bool _yes) { - yes = _yes; - } +import {Authority} from "../auth/Auth.sol"; +contract OutOfOrderAuthority is Authority { function canCall( address, address, bytes4 - ) public view override returns (bool) { - return yes; + ) public pure override returns (bool) { + revert("OUT_OF_ORDER"); } } @@ -29,39 +24,167 @@ contract AuthTest is DSTestPlus { mockAuthChild = new MockAuthChild(); } - function invariantOwner() public { - assertEq(mockAuthChild.owner(), address(this)); + function testSetOwnerAsOwner() public { + mockAuthChild.setOwner(address(0xBEEF)); + assertEq(mockAuthChild.owner(), address(0xBEEF)); + } + + function testSetAuthorityAsOwner() public { + mockAuthChild.setAuthority(Authority(address(0xBEEF))); + assertEq(address(mockAuthChild.authority()), address(0xBEEF)); } - function invariantAuthority() public { - assertEq(address(mockAuthChild.authority()), address(0)); + function testCallFunctionAsOwner() public { + mockAuthChild.updateFlag(); } - function testFailNonOwner1() public { + function testSetOwnerWithPermissiveAuthority() public { + mockAuthChild.setAuthority(new MockAuthority(true)); + mockAuthChild.setOwner(address(0)); + mockAuthChild.setOwner(address(this)); + } + + function testSetAuthorityWithPermissiveAuthority() public { + mockAuthChild.setAuthority(new MockAuthority(true)); + mockAuthChild.setOwner(address(0)); + mockAuthChild.setAuthority(Authority(address(0xBEEF))); + } + + function testCallFunctionWithPermissiveAuthority() public { + mockAuthChild.setAuthority(new MockAuthority(true)); mockAuthChild.setOwner(address(0)); mockAuthChild.updateFlag(); } - function testFailNonOwner2() public { + function testSetAuthorityAsOwnerWithOutOfOrderAuthority() public { + mockAuthChild.setAuthority(new OutOfOrderAuthority()); + mockAuthChild.setAuthority(new MockAuthority(true)); + } + + function testFailSetOwnerAsNonOwner() public { mockAuthChild.setOwner(address(0)); + mockAuthChild.setOwner(address(0xBEEF)); + } + + function testFailSetAuthorityAsNonOwner() public { mockAuthChild.setOwner(address(0)); + mockAuthChild.setAuthority(Authority(address(0xBEEF))); } - function testFailRejectingAuthority1() public { - mockAuthChild.setAuthority(Authority(address(new BooleanAuthority(false)))); + function testFailCallFunctionAsNonOwner() public { mockAuthChild.setOwner(address(0)); mockAuthChild.updateFlag(); } - function testFailRejectingAuthority2() public { - mockAuthChild.setAuthority(Authority(address(new BooleanAuthority(false)))); + function testFailSetOwnerWithRestrictiveAuthority() public { + mockAuthChild.setAuthority(new MockAuthority(false)); + mockAuthChild.setOwner(address(0)); + mockAuthChild.setOwner(address(this)); + } + + function testFailSetAuthorityWithRestrictiveAuthority() public { + mockAuthChild.setAuthority(new MockAuthority(false)); mockAuthChild.setOwner(address(0)); + mockAuthChild.setAuthority(Authority(address(0xBEEF))); + } + + function testFailCallFunctionWithRestrictiveAuthority() public { + mockAuthChild.setAuthority(new MockAuthority(false)); mockAuthChild.setOwner(address(0)); + mockAuthChild.updateFlag(); } - function testAcceptingOwner() public { - mockAuthChild.setAuthority(Authority(address(new BooleanAuthority(true)))); + function testFailSetOwnerAsOwnerWithOutOfOrderAuthority() public { + mockAuthChild.setAuthority(new OutOfOrderAuthority()); mockAuthChild.setOwner(address(0)); + } + + function testFailCallFunctionAsOwnerWithOutOfOrderAuthority() public { + mockAuthChild.setAuthority(new OutOfOrderAuthority()); + mockAuthChild.updateFlag(); + } + + function testSetOwnerAsOwner(address newOwner) public { + mockAuthChild.setOwner(newOwner); + assertEq(mockAuthChild.owner(), newOwner); + } + + function testSetAuthorityAsOwner(Authority newAuthority) public { + mockAuthChild.setAuthority(newAuthority); + assertEq(address(mockAuthChild.authority()), address(newAuthority)); + } + + function testSetOwnerWithPermissiveAuthority(address deadOwner, address newOwner) public { + if (deadOwner == address(this)) deadOwner = address(0); + + mockAuthChild.setAuthority(new MockAuthority(true)); + mockAuthChild.setOwner(deadOwner); + mockAuthChild.setOwner(newOwner); + } + + function testSetAuthorityWithPermissiveAuthority(address deadOwner, Authority newAuthority) public { + if (deadOwner == address(this)) deadOwner = address(0); + + mockAuthChild.setAuthority(new MockAuthority(true)); + mockAuthChild.setOwner(deadOwner); + mockAuthChild.setAuthority(newAuthority); + } + + function testCallFunctionWithPermissiveAuthority(address deadOwner) public { + if (deadOwner == address(this)) deadOwner = address(0); + + mockAuthChild.setAuthority(new MockAuthority(true)); + mockAuthChild.setOwner(deadOwner); + mockAuthChild.updateFlag(); + } + + function testFailSetOwnerAsNonOwner(address deadOwner, address newOwner) public { + if (deadOwner == address(this)) deadOwner = address(0); + + mockAuthChild.setOwner(deadOwner); + mockAuthChild.setOwner(newOwner); + } + + function testFailSetAuthorityAsNonOwner(address deadOwner, Authority newAuthority) public { + mockAuthChild.setOwner(deadOwner); + mockAuthChild.setAuthority(newAuthority); + } + + function testFailCallFunctionAsNonOwner(address deadOwner) public { + if (deadOwner == address(this)) deadOwner = address(0); + + mockAuthChild.setOwner(deadOwner); mockAuthChild.updateFlag(); } + + function testFailSetOwnerWithRestrictiveAuthority(address deadOwner, address newOwner) public { + if (deadOwner == address(this)) deadOwner = address(0); + + mockAuthChild.setAuthority(new MockAuthority(false)); + mockAuthChild.setOwner(deadOwner); + mockAuthChild.setOwner(newOwner); + } + + function testFailSetAuthorityWithRestrictiveAuthority(address deadOwner, Authority newAuthority) public { + if (deadOwner == address(this)) deadOwner = address(0); + + mockAuthChild.setAuthority(new MockAuthority(false)); + mockAuthChild.setOwner(deadOwner); + mockAuthChild.setAuthority(newAuthority); + } + + function testFailCallFunctionWithRestrictiveAuthority(address deadOwner) public { + if (deadOwner == address(this)) deadOwner = address(0); + + mockAuthChild.setAuthority(new MockAuthority(false)); + mockAuthChild.setOwner(deadOwner); + mockAuthChild.updateFlag(); + } + + function testFailSetOwnerAsOwnerWithOutOfOrderAuthority(address deadOwner) public { + if (deadOwner == address(this)) deadOwner = address(0); + + mockAuthChild.setAuthority(new OutOfOrderAuthority()); + mockAuthChild.setOwner(deadOwner); + } } diff --git a/src/test/CREATE3.t.sol b/src/test/CREATE3.t.sol index a6f671bd..8120632d 100644 --- a/src/test/CREATE3.t.sol +++ b/src/test/CREATE3.t.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity 0.8.10; +import {WETH} from "../tokens/WETH.sol"; import {DSTestPlus} from "./utils/DSTestPlus.sol"; import {MockERC20} from "./utils/mocks/MockERC20.sol"; import {MockAuthChild} from "./utils/mocks/MockAuthChild.sol"; -import {MockTrustChild} from "./utils/mocks/MockTrustChild.sol"; import {CREATE3} from "../utils/CREATE3.sol"; @@ -13,7 +13,11 @@ contract CREATE3Test is DSTestPlus { bytes32 salt = keccak256(bytes("A salt!")); MockERC20 deployed = MockERC20( - CREATE3.deploy(salt, abi.encodePacked(type(MockERC20).creationCode, abi.encode("Mock Token", "MOCK", 18))) + CREATE3.deploy( + salt, + abi.encodePacked(type(MockERC20).creationCode, abi.encode("Mock Token", "MOCK", 18)), + 0 + ) ); assertEq(address(deployed), CREATE3.getDeployed(salt)); @@ -26,15 +30,15 @@ contract CREATE3Test is DSTestPlus { function testFailDoubleDeploySameBytecode() public { bytes32 salt = keccak256(bytes("Salty...")); - CREATE3.deploy(salt, type(MockAuthChild).creationCode); - CREATE3.deploy(salt, type(MockAuthChild).creationCode); + CREATE3.deploy(salt, type(MockAuthChild).creationCode, 0); + CREATE3.deploy(salt, type(MockAuthChild).creationCode, 0); } function testFailDoubleDeployDifferentBytecode() public { bytes32 salt = keccak256(bytes("and sweet!")); - CREATE3.deploy(salt, type(MockAuthChild).creationCode); - CREATE3.deploy(salt, type(MockTrustChild).creationCode); + CREATE3.deploy(salt, type(WETH).creationCode, 0); + CREATE3.deploy(salt, type(MockAuthChild).creationCode, 0); } function testDeployERC20( @@ -44,7 +48,7 @@ contract CREATE3Test is DSTestPlus { uint8 decimals ) public { MockERC20 deployed = MockERC20( - CREATE3.deploy(salt, abi.encodePacked(type(MockERC20).creationCode, abi.encode(name, symbol, decimals))) + CREATE3.deploy(salt, abi.encodePacked(type(MockERC20).creationCode, abi.encode(name, symbol, decimals)), 0) ); assertEq(address(deployed), CREATE3.getDeployed(salt)); @@ -55,8 +59,8 @@ contract CREATE3Test is DSTestPlus { } function testFailDoubleDeploySameBytecode(bytes32 salt, bytes calldata bytecode) public { - CREATE3.deploy(salt, bytecode); - CREATE3.deploy(salt, bytecode); + CREATE3.deploy(salt, bytecode, 0); + CREATE3.deploy(salt, bytecode, 0); } function testFailDoubleDeployDifferentBytecode( @@ -64,7 +68,7 @@ contract CREATE3Test is DSTestPlus { bytes calldata bytecode1, bytes calldata bytecode2 ) public { - CREATE3.deploy(salt, bytecode1); - CREATE3.deploy(salt, bytecode2); + CREATE3.deploy(salt, bytecode1, 0); + CREATE3.deploy(salt, bytecode2, 0); } } diff --git a/src/test/DSTestPlus.t.sol b/src/test/DSTestPlus.t.sol new file mode 100644 index 00000000..432cd327 --- /dev/null +++ b/src/test/DSTestPlus.t.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.10; + +import {DSTestPlus} from "./utils/DSTestPlus.sol"; + +contract DSTestPlusTest is DSTestPlus { + function testBound() public { + assertEq(bound(5, 0, 4), 0); + assertEq(bound(0, 69, 69), 69); + assertEq(bound(0, 68, 69), 68); + assertEq(bound(10, 150, 190), 174); + assertEq(bound(300, 2800, 3200), 3107); + assertEq(bound(9999, 1337, 6666), 4669); + } + + function testFailBoundMinBiggerThanMax() public pure { + bound(5, 100, 10); + } + + function testBound( + uint256 num, + uint256 min, + uint256 max + ) public { + if (min > max) (min, max) = (max, min); + + uint256 bounded = bound(num, min, max); + + assertGe(bounded, min); + assertLe(bounded, max); + } + + function testFailBoundMinBiggerThanMax( + uint256 num, + uint256 min, + uint256 max + ) public pure { + if (max == min) { + unchecked { + min++; // Overflow is handled below. + } + } + + if (max > min) (min, max) = (max, min); + + bound(num, min, max); + } +} diff --git a/src/test/ERC1155.t.sol b/src/test/ERC1155.t.sol new file mode 100644 index 00000000..ce5ef058 --- /dev/null +++ b/src/test/ERC1155.t.sol @@ -0,0 +1,1777 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.10; + +import {DSTestPlus} from "./utils/DSTestPlus.sol"; +import {DSInvariantTest} from "./utils/DSInvariantTest.sol"; + +import {MockERC1155} from "./utils/mocks/MockERC1155.sol"; +import {ERC1155User} from "./utils/users/ERC1155User.sol"; + +import {ERC1155TokenReceiver} from "../tokens/ERC1155.sol"; + +contract ERC1155Recipient is ERC1155TokenReceiver { + address public operator; + address public from; + uint256 public id; + uint256 public amount; + bytes public mintData; + + function onERC1155Received( + address _operator, + address _from, + uint256 _id, + uint256 _amount, + bytes calldata _data + ) public override returns (bytes4) { + operator = _operator; + from = _from; + id = _id; + amount = _amount; + mintData = _data; + + return ERC1155TokenReceiver.onERC1155Received.selector; + } + + address public batchOperator; + address public batchFrom; + uint256[] internal _batchIds; + uint256[] internal _batchAmounts; + bytes public batchData; + + function batchIds() external view returns (uint256[] memory) { + return _batchIds; + } + + function batchAmounts() external view returns (uint256[] memory) { + return _batchAmounts; + } + + function onERC1155BatchReceived( + address _operator, + address _from, + uint256[] calldata _ids, + uint256[] calldata _amounts, + bytes calldata _data + ) external override returns (bytes4) { + batchOperator = _operator; + batchFrom = _from; + _batchIds = _ids; + _batchAmounts = _amounts; + batchData = _data; + + return ERC1155TokenReceiver.onERC1155BatchReceived.selector; + } +} + +contract RevertingERC1155Recipient is ERC1155TokenReceiver { + function onERC1155Received( + address, + address, + uint256, + uint256, + bytes calldata + ) public pure override returns (bytes4) { + revert(string(abi.encodePacked(ERC1155TokenReceiver.onERC1155Received.selector))); + } + + function onERC1155BatchReceived( + address, + address, + uint256[] calldata, + uint256[] calldata, + bytes calldata + ) external pure override returns (bytes4) { + revert(string(abi.encodePacked(ERC1155TokenReceiver.onERC1155BatchReceived.selector))); + } +} + +contract WrongReturnDataERC1155Recipient is ERC1155TokenReceiver { + function onERC1155Received( + address, + address, + uint256, + uint256, + bytes calldata + ) public pure override returns (bytes4) { + return 0xCAFEBEEF; + } + + function onERC1155BatchReceived( + address, + address, + uint256[] calldata, + uint256[] calldata, + bytes calldata + ) external pure override returns (bytes4) { + return 0xCAFEBEEF; + } +} + +contract NonERC1155Recipient {} + +contract ERC1155Test is DSTestPlus, ERC1155TokenReceiver { + MockERC1155 token; + + mapping(address => mapping(uint256 => uint256)) public userMintAmounts; + mapping(address => mapping(uint256 => uint256)) public userTransferOrBurnAmounts; + + function setUp() public { + token = new MockERC1155(); + } + + function testMintToEOA() public { + token.mint(address(0xBEEF), 1337, 1, ""); + + assertEq(token.balanceOf(address(0xBEEF), 1337), 1); + } + + function testMintToERC1155Recipient() public { + ERC1155Recipient to = new ERC1155Recipient(); + + token.mint(address(to), 1337, 1, "testing 123"); + + assertEq(token.balanceOf(address(to), 1337), 1); + + assertEq(to.operator(), address(this)); + assertEq(to.from(), address(0)); + assertEq(to.id(), 1337); + assertBytesEq(to.mintData(), "testing 123"); + } + + function testBatchMintToEOA() public { + uint256[] memory ids = new uint256[](5); + ids[0] = 1337; + ids[1] = 1338; + ids[2] = 1339; + ids[3] = 1340; + ids[4] = 1341; + + uint256[] memory amounts = new uint256[](5); + amounts[0] = 100; + amounts[1] = 200; + amounts[2] = 300; + amounts[3] = 400; + amounts[4] = 500; + + token.batchMint(address(0xBEEF), ids, amounts, ""); + + assertEq(token.balanceOf(address(0xBEEF), 1337), 100); + assertEq(token.balanceOf(address(0xBEEF), 1338), 200); + assertEq(token.balanceOf(address(0xBEEF), 1339), 300); + assertEq(token.balanceOf(address(0xBEEF), 1340), 400); + assertEq(token.balanceOf(address(0xBEEF), 1341), 500); + } + + function testBatchMintToERC1155Recipient() public { + ERC1155Recipient to = new ERC1155Recipient(); + + uint256[] memory ids = new uint256[](5); + ids[0] = 1337; + ids[1] = 1338; + ids[2] = 1339; + ids[3] = 1340; + ids[4] = 1341; + + uint256[] memory amounts = new uint256[](5); + amounts[0] = 100; + amounts[1] = 200; + amounts[2] = 300; + amounts[3] = 400; + amounts[4] = 500; + + token.batchMint(address(to), ids, amounts, "testing 123"); + + assertEq(to.batchOperator(), address(this)); + assertEq(to.batchFrom(), address(0)); + assertUintArrayEq(to.batchIds(), ids); + assertUintArrayEq(to.batchAmounts(), amounts); + assertBytesEq(to.batchData(), "testing 123"); + + assertEq(token.balanceOf(address(to), 1337), 100); + assertEq(token.balanceOf(address(to), 1338), 200); + assertEq(token.balanceOf(address(to), 1339), 300); + assertEq(token.balanceOf(address(to), 1340), 400); + assertEq(token.balanceOf(address(to), 1341), 500); + } + + function testBurn() public { + token.mint(address(0xBEEF), 1337, 100, ""); + + token.burn(address(0xBEEF), 1337, 70); + + assertEq(token.balanceOf(address(0xBEEF), 1337), 30); + } + + function testBatchBurn() public { + uint256[] memory ids = new uint256[](5); + ids[0] = 1337; + ids[1] = 1338; + ids[2] = 1339; + ids[3] = 1340; + ids[4] = 1341; + + uint256[] memory mintAmounts = new uint256[](5); + mintAmounts[0] = 100; + mintAmounts[1] = 200; + mintAmounts[2] = 300; + mintAmounts[3] = 400; + mintAmounts[4] = 500; + + uint256[] memory burnAmounts = new uint256[](5); + burnAmounts[0] = 50; + burnAmounts[1] = 100; + burnAmounts[2] = 150; + burnAmounts[3] = 200; + burnAmounts[4] = 250; + + token.batchMint(address(0xBEEF), ids, mintAmounts, ""); + + token.batchBurn(address(0xBEEF), ids, burnAmounts); + + assertEq(token.balanceOf(address(0xBEEF), 1337), 50); + assertEq(token.balanceOf(address(0xBEEF), 1338), 100); + assertEq(token.balanceOf(address(0xBEEF), 1339), 150); + assertEq(token.balanceOf(address(0xBEEF), 1340), 200); + assertEq(token.balanceOf(address(0xBEEF), 1341), 250); + } + + function testApproveAll() public { + token.setApprovalForAll(address(0xBEEF), true); + + assertTrue(token.isApprovedForAll(address(this), address(0xBEEF))); + } + + function testSafeTransferFromToEOA() public { + ERC1155User from = new ERC1155User(token); + + token.mint(address(from), 1337, 100, ""); + + from.setApprovalForAll(address(this), true); + + token.safeTransferFrom(address(from), address(0xBEEF), 1337, 70, ""); + + assertEq(token.balanceOf(address(0xBEEF), 1337), 70); + assertEq(token.balanceOf(address(from), 1337), 30); + } + + function testSafeTransferFromToERC1155Recipient() public { + ERC1155Recipient to = new ERC1155Recipient(); + + ERC1155User from = new ERC1155User(token); + + token.mint(address(from), 1337, 100, ""); + + from.setApprovalForAll(address(this), true); + + token.safeTransferFrom(address(from), address(to), 1337, 70, "testing 123"); + + assertEq(to.operator(), address(this)); + assertEq(to.from(), address(from)); + assertEq(to.id(), 1337); + assertBytesEq(to.mintData(), "testing 123"); + + assertEq(token.balanceOf(address(to), 1337), 70); + assertEq(token.balanceOf(address(from), 1337), 30); + } + + function testSafeTransferFromSelf() public { + token.mint(address(this), 1337, 100, ""); + + token.safeTransferFrom(address(this), address(0xBEEF), 1337, 70, ""); + + assertEq(token.balanceOf(address(0xBEEF), 1337), 70); + assertEq(token.balanceOf(address(this), 1337), 30); + } + + function testSafeBatchTransferFromToEOA() public { + ERC1155User from = new ERC1155User(token); + + uint256[] memory ids = new uint256[](5); + ids[0] = 1337; + ids[1] = 1338; + ids[2] = 1339; + ids[3] = 1340; + ids[4] = 1341; + + uint256[] memory mintAmounts = new uint256[](5); + mintAmounts[0] = 100; + mintAmounts[1] = 200; + mintAmounts[2] = 300; + mintAmounts[3] = 400; + mintAmounts[4] = 500; + + uint256[] memory transferAmounts = new uint256[](5); + transferAmounts[0] = 50; + transferAmounts[1] = 100; + transferAmounts[2] = 150; + transferAmounts[3] = 200; + transferAmounts[4] = 250; + + token.batchMint(address(from), ids, mintAmounts, ""); + + from.setApprovalForAll(address(this), true); + + token.safeBatchTransferFrom(address(from), address(0xBEEF), ids, transferAmounts, ""); + + assertEq(token.balanceOf(address(from), 1337), 50); + assertEq(token.balanceOf(address(0xBEEF), 1337), 50); + + assertEq(token.balanceOf(address(from), 1338), 100); + assertEq(token.balanceOf(address(0xBEEF), 1338), 100); + + assertEq(token.balanceOf(address(from), 1339), 150); + assertEq(token.balanceOf(address(0xBEEF), 1339), 150); + + assertEq(token.balanceOf(address(from), 1340), 200); + assertEq(token.balanceOf(address(0xBEEF), 1340), 200); + + assertEq(token.balanceOf(address(from), 1341), 250); + assertEq(token.balanceOf(address(0xBEEF), 1341), 250); + } + + function testSafeBatchTransferFromToERC1155Recipient() public { + ERC1155User from = new ERC1155User(token); + + ERC1155Recipient to = new ERC1155Recipient(); + + uint256[] memory ids = new uint256[](5); + ids[0] = 1337; + ids[1] = 1338; + ids[2] = 1339; + ids[3] = 1340; + ids[4] = 1341; + + uint256[] memory mintAmounts = new uint256[](5); + mintAmounts[0] = 100; + mintAmounts[1] = 200; + mintAmounts[2] = 300; + mintAmounts[3] = 400; + mintAmounts[4] = 500; + + uint256[] memory transferAmounts = new uint256[](5); + transferAmounts[0] = 50; + transferAmounts[1] = 100; + transferAmounts[2] = 150; + transferAmounts[3] = 200; + transferAmounts[4] = 250; + + token.batchMint(address(from), ids, mintAmounts, ""); + + from.setApprovalForAll(address(this), true); + + token.safeBatchTransferFrom(address(from), address(to), ids, transferAmounts, "testing 123"); + + assertEq(to.batchOperator(), address(this)); + assertEq(to.batchFrom(), address(from)); + assertUintArrayEq(to.batchIds(), ids); + assertUintArrayEq(to.batchAmounts(), transferAmounts); + assertBytesEq(to.batchData(), "testing 123"); + + assertEq(token.balanceOf(address(from), 1337), 50); + assertEq(token.balanceOf(address(to), 1337), 50); + + assertEq(token.balanceOf(address(from), 1338), 100); + assertEq(token.balanceOf(address(to), 1338), 100); + + assertEq(token.balanceOf(address(from), 1339), 150); + assertEq(token.balanceOf(address(to), 1339), 150); + + assertEq(token.balanceOf(address(from), 1340), 200); + assertEq(token.balanceOf(address(to), 1340), 200); + + assertEq(token.balanceOf(address(from), 1341), 250); + assertEq(token.balanceOf(address(to), 1341), 250); + } + + function testBatchBalanceOf() public { + address[] memory tos = new address[](5); + tos[0] = address(0xBEEF); + tos[1] = address(0xCAFE); + tos[2] = address(0xFACE); + tos[3] = address(0xDEAD); + tos[4] = address(0xFEED); + + uint256[] memory ids = new uint256[](5); + ids[0] = 1337; + ids[1] = 1338; + ids[2] = 1339; + ids[3] = 1340; + ids[4] = 1341; + + token.mint(address(0xBEEF), 1337, 100, ""); + token.mint(address(0xCAFE), 1338, 200, ""); + token.mint(address(0xFACE), 1339, 300, ""); + token.mint(address(0xDEAD), 1340, 400, ""); + token.mint(address(0xFEED), 1341, 500, ""); + + uint256[] memory balances = token.balanceOfBatch(tos, ids); + + assertEq(balances[0], 100); + assertEq(balances[1], 200); + assertEq(balances[2], 300); + assertEq(balances[3], 400); + assertEq(balances[4], 500); + } + + function testFailMintToZero() public { + token.mint(address(0), 1337, 1, ""); + } + + function testFailMintToNonERC155Recipient() public { + token.mint(address(new NonERC1155Recipient()), 1337, 1, ""); + } + + function testFailMintToRevertingERC155Recipient() public { + token.mint(address(new RevertingERC1155Recipient()), 1337, 1, ""); + } + + function testFailMintToWrongReturnDataERC155Recipient() public { + token.mint(address(new RevertingERC1155Recipient()), 1337, 1, ""); + } + + function testFailBurnInsufficientBalance() public { + token.mint(address(0xBEEF), 1337, 70, ""); + token.burn(address(0xBEEF), 1337, 100); + } + + function testFailSafeTransferFromInsufficientBalance() public { + ERC1155User from = new ERC1155User(token); + + token.mint(address(from), 1337, 70, ""); + + from.setApprovalForAll(address(this), true); + + token.safeTransferFrom(address(from), address(0xBEEF), 1337, 100, ""); + } + + function testFailSafeTransferFromSelfInsufficientBalance() public { + token.mint(address(this), 1337, 70, ""); + token.safeTransferFrom(address(this), address(0xBEEF), 1337, 100, ""); + } + + function testFailSafeTransferFromToZero() public { + token.mint(address(this), 1337, 100, ""); + token.safeTransferFrom(address(this), address(0), 1337, 70, ""); + } + + function testFailSafeTransferFromToNonERC155Recipient() public { + token.mint(address(this), 1337, 100, ""); + token.safeTransferFrom(address(this), address(new NonERC1155Recipient()), 1337, 70, ""); + } + + function testFailSafeTransferFromToRevertingERC1155Recipient() public { + token.mint(address(this), 1337, 100, ""); + token.safeTransferFrom(address(this), address(new RevertingERC1155Recipient()), 1337, 70, ""); + } + + function testFailSafeTransferFromToWrongReturnDataERC1155Recipient() public { + token.mint(address(this), 1337, 100, ""); + token.safeTransferFrom(address(this), address(new WrongReturnDataERC1155Recipient()), 1337, 70, ""); + } + + function testFailSafeBatchTransferInsufficientBalance() public { + ERC1155User from = new ERC1155User(token); + + uint256[] memory ids = new uint256[](5); + ids[0] = 1337; + ids[1] = 1338; + ids[2] = 1339; + ids[3] = 1340; + ids[4] = 1341; + + uint256[] memory mintAmounts = new uint256[](5); + + mintAmounts[0] = 50; + mintAmounts[1] = 100; + mintAmounts[2] = 150; + mintAmounts[3] = 200; + mintAmounts[4] = 250; + + uint256[] memory transferAmounts = new uint256[](5); + transferAmounts[0] = 100; + transferAmounts[1] = 200; + transferAmounts[2] = 300; + transferAmounts[3] = 400; + transferAmounts[4] = 500; + + token.batchMint(address(from), ids, mintAmounts, ""); + + from.setApprovalForAll(address(this), true); + + token.safeBatchTransferFrom(address(from), address(0xBEEF), ids, transferAmounts, ""); + } + + function testFailSafeBatchTransferFromToZero() public { + ERC1155User from = new ERC1155User(token); + + uint256[] memory ids = new uint256[](5); + ids[0] = 1337; + ids[1] = 1338; + ids[2] = 1339; + ids[3] = 1340; + ids[4] = 1341; + + uint256[] memory mintAmounts = new uint256[](5); + mintAmounts[0] = 100; + mintAmounts[1] = 200; + mintAmounts[2] = 300; + mintAmounts[3] = 400; + mintAmounts[4] = 500; + + uint256[] memory transferAmounts = new uint256[](5); + transferAmounts[0] = 50; + transferAmounts[1] = 100; + transferAmounts[2] = 150; + transferAmounts[3] = 200; + transferAmounts[4] = 250; + + token.batchMint(address(from), ids, mintAmounts, ""); + + from.setApprovalForAll(address(this), true); + + token.safeBatchTransferFrom(address(from), address(0), ids, transferAmounts, ""); + } + + function testFailSafeBatchTransferFromToNonERC1155Recipient() public { + ERC1155User from = new ERC1155User(token); + + uint256[] memory ids = new uint256[](5); + ids[0] = 1337; + ids[1] = 1338; + ids[2] = 1339; + ids[3] = 1340; + ids[4] = 1341; + + uint256[] memory mintAmounts = new uint256[](5); + mintAmounts[0] = 100; + mintAmounts[1] = 200; + mintAmounts[2] = 300; + mintAmounts[3] = 400; + mintAmounts[4] = 500; + + uint256[] memory transferAmounts = new uint256[](5); + transferAmounts[0] = 50; + transferAmounts[1] = 100; + transferAmounts[2] = 150; + transferAmounts[3] = 200; + transferAmounts[4] = 250; + + token.batchMint(address(from), ids, mintAmounts, ""); + + from.setApprovalForAll(address(this), true); + + token.safeBatchTransferFrom(address(from), address(new NonERC1155Recipient()), ids, transferAmounts, ""); + } + + function testFailSafeBatchTransferFromToRevertingERC1155Recipient() public { + ERC1155User from = new ERC1155User(token); + + uint256[] memory ids = new uint256[](5); + ids[0] = 1337; + ids[1] = 1338; + ids[2] = 1339; + ids[3] = 1340; + ids[4] = 1341; + + uint256[] memory mintAmounts = new uint256[](5); + mintAmounts[0] = 100; + mintAmounts[1] = 200; + mintAmounts[2] = 300; + mintAmounts[3] = 400; + mintAmounts[4] = 500; + + uint256[] memory transferAmounts = new uint256[](5); + transferAmounts[0] = 50; + transferAmounts[1] = 100; + transferAmounts[2] = 150; + transferAmounts[3] = 200; + transferAmounts[4] = 250; + + token.batchMint(address(from), ids, mintAmounts, ""); + + from.setApprovalForAll(address(this), true); + + token.safeBatchTransferFrom(address(from), address(new RevertingERC1155Recipient()), ids, transferAmounts, ""); + } + + function testFailSafeBatchTransferFromToWrongReturnDataERC1155Recipient() public { + ERC1155User from = new ERC1155User(token); + + uint256[] memory ids = new uint256[](5); + ids[0] = 1337; + ids[1] = 1338; + ids[2] = 1339; + ids[3] = 1340; + ids[4] = 1341; + + uint256[] memory mintAmounts = new uint256[](5); + mintAmounts[0] = 100; + mintAmounts[1] = 200; + mintAmounts[2] = 300; + mintAmounts[3] = 400; + mintAmounts[4] = 500; + + uint256[] memory transferAmounts = new uint256[](5); + transferAmounts[0] = 50; + transferAmounts[1] = 100; + transferAmounts[2] = 150; + transferAmounts[3] = 200; + transferAmounts[4] = 250; + + token.batchMint(address(from), ids, mintAmounts, ""); + + from.setApprovalForAll(address(this), true); + + token.safeBatchTransferFrom( + address(from), + address(new WrongReturnDataERC1155Recipient()), + ids, + transferAmounts, + "" + ); + } + + function testFailSafeBatchTransferFromWithArrayLengthMismatch() public { + ERC1155User from = new ERC1155User(token); + + uint256[] memory ids = new uint256[](5); + ids[0] = 1337; + ids[1] = 1338; + ids[2] = 1339; + ids[3] = 1340; + ids[4] = 1341; + + uint256[] memory mintAmounts = new uint256[](5); + mintAmounts[0] = 100; + mintAmounts[1] = 200; + mintAmounts[2] = 300; + mintAmounts[3] = 400; + mintAmounts[4] = 500; + + uint256[] memory transferAmounts = new uint256[](4); + transferAmounts[0] = 50; + transferAmounts[1] = 100; + transferAmounts[2] = 150; + transferAmounts[3] = 200; + + token.batchMint(address(from), ids, mintAmounts, ""); + + from.setApprovalForAll(address(this), true); + + token.safeBatchTransferFrom(address(from), address(0xBEEF), ids, transferAmounts, ""); + } + + function testFailBatchMintToZero() public { + uint256[] memory ids = new uint256[](5); + ids[0] = 1337; + ids[1] = 1338; + ids[2] = 1339; + ids[3] = 1340; + ids[4] = 1341; + + uint256[] memory mintAmounts = new uint256[](5); + mintAmounts[0] = 100; + mintAmounts[1] = 200; + mintAmounts[2] = 300; + mintAmounts[3] = 400; + mintAmounts[4] = 500; + + token.batchMint(address(0), ids, mintAmounts, ""); + } + + function testFailBatchMintToNonERC1155Recipient() public { + NonERC1155Recipient to = new NonERC1155Recipient(); + + uint256[] memory ids = new uint256[](5); + ids[0] = 1337; + ids[1] = 1338; + ids[2] = 1339; + ids[3] = 1340; + ids[4] = 1341; + + uint256[] memory mintAmounts = new uint256[](5); + mintAmounts[0] = 100; + mintAmounts[1] = 200; + mintAmounts[2] = 300; + mintAmounts[3] = 400; + mintAmounts[4] = 500; + + token.batchMint(address(to), ids, mintAmounts, ""); + } + + function testFailBatchMintToRevertingERC1155Recipient() public { + RevertingERC1155Recipient to = new RevertingERC1155Recipient(); + + uint256[] memory ids = new uint256[](5); + ids[0] = 1337; + ids[1] = 1338; + ids[2] = 1339; + ids[3] = 1340; + ids[4] = 1341; + + uint256[] memory mintAmounts = new uint256[](5); + mintAmounts[0] = 100; + mintAmounts[1] = 200; + mintAmounts[2] = 300; + mintAmounts[3] = 400; + mintAmounts[4] = 500; + + token.batchMint(address(to), ids, mintAmounts, ""); + } + + function testFailBatchMintToWrongReturnDataERC1155Recipient() public { + WrongReturnDataERC1155Recipient to = new WrongReturnDataERC1155Recipient(); + + uint256[] memory ids = new uint256[](5); + ids[0] = 1337; + ids[1] = 1338; + ids[2] = 1339; + ids[3] = 1340; + ids[4] = 1341; + + uint256[] memory mintAmounts = new uint256[](5); + mintAmounts[0] = 100; + mintAmounts[1] = 200; + mintAmounts[2] = 300; + mintAmounts[3] = 400; + mintAmounts[4] = 500; + + token.batchMint(address(to), ids, mintAmounts, ""); + } + + function testFailBatchMintWithArrayMismatch() public { + uint256[] memory ids = new uint256[](5); + ids[0] = 1337; + ids[1] = 1338; + ids[2] = 1339; + ids[3] = 1340; + ids[4] = 1341; + + uint256[] memory amounts = new uint256[](4); + amounts[0] = 100; + amounts[1] = 200; + amounts[2] = 300; + amounts[3] = 400; + + token.batchMint(address(0xBEEF), ids, amounts, ""); + } + + function testFailBatchBurnInsufficientBalance() public { + uint256[] memory ids = new uint256[](5); + ids[0] = 1337; + ids[1] = 1338; + ids[2] = 1339; + ids[3] = 1340; + ids[4] = 1341; + + uint256[] memory mintAmounts = new uint256[](5); + mintAmounts[0] = 50; + mintAmounts[1] = 100; + mintAmounts[2] = 150; + mintAmounts[3] = 200; + mintAmounts[4] = 250; + + uint256[] memory burnAmounts = new uint256[](5); + burnAmounts[0] = 100; + burnAmounts[1] = 200; + burnAmounts[2] = 300; + burnAmounts[3] = 400; + burnAmounts[4] = 500; + + token.batchMint(address(0xBEEF), ids, mintAmounts, ""); + + token.batchBurn(address(0xBEEF), ids, burnAmounts); + } + + function testFailBatchBurnWithArrayLengthMismatch() public { + uint256[] memory ids = new uint256[](5); + ids[0] = 1337; + ids[1] = 1338; + ids[2] = 1339; + ids[3] = 1340; + ids[4] = 1341; + + uint256[] memory mintAmounts = new uint256[](5); + mintAmounts[0] = 100; + mintAmounts[1] = 200; + mintAmounts[2] = 300; + mintAmounts[3] = 400; + mintAmounts[4] = 500; + + uint256[] memory burnAmounts = new uint256[](4); + burnAmounts[0] = 50; + burnAmounts[1] = 100; + burnAmounts[2] = 150; + burnAmounts[3] = 200; + + token.batchMint(address(0xBEEF), ids, mintAmounts, ""); + + token.batchBurn(address(0xBEEF), ids, burnAmounts); + } + + function testFailBalanceOfBatchWithArrayMismatch() public view { + address[] memory tos = new address[](5); + tos[0] = address(0xBEEF); + tos[1] = address(0xCAFE); + tos[2] = address(0xFACE); + tos[3] = address(0xDEAD); + tos[4] = address(0xFEED); + + uint256[] memory ids = new uint256[](4); + ids[0] = 1337; + ids[1] = 1338; + ids[2] = 1339; + ids[3] = 1340; + + token.balanceOfBatch(tos, ids); + } + + function testMintToEOA( + address to, + uint256 id, + uint256 amount, + bytes memory mintData + ) public { + if (to == address(0)) to = address(0xBEEF); + + if (uint256(uint160(to)) <= 18 || to.code.length > 0) return; + + token.mint(to, id, amount, mintData); + + assertEq(token.balanceOf(to, id), amount); + } + + function testMintToERC1155Recipient( + uint256 id, + uint256 amount, + bytes memory mintData + ) public { + ERC1155Recipient to = new ERC1155Recipient(); + + token.mint(address(to), id, amount, mintData); + + assertEq(token.balanceOf(address(to), id), amount); + + assertEq(to.operator(), address(this)); + assertEq(to.from(), address(0)); + assertEq(to.id(), id); + assertBytesEq(to.mintData(), mintData); + } + + function testBatchMintToEOA( + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory mintData + ) public { + if (to == address(0)) to = address(0xBEEF); + + if (uint256(uint160(to)) <= 18 || to.code.length > 0) return; + + uint256 minLength = min2(ids.length, amounts.length); + + uint256[] memory normalizedIds = new uint256[](minLength); + uint256[] memory normalizedAmounts = new uint256[](minLength); + + for (uint256 i = 0; i < minLength; i++) { + uint256 id = ids[i]; + + uint256 remainingMintAmountForId = type(uint256).max - userMintAmounts[to][id]; + + uint256 mintAmount = bound(amounts[i], 0, remainingMintAmountForId); + + normalizedIds[i] = id; + normalizedAmounts[i] = mintAmount; + + userMintAmounts[to][id] += mintAmount; + } + + token.batchMint(to, normalizedIds, normalizedAmounts, mintData); + + for (uint256 i = 0; i < normalizedIds.length; i++) { + uint256 id = normalizedIds[i]; + + assertEq(token.balanceOf(to, id), userMintAmounts[to][id]); + } + } + + function testBatchMintToERC1155Recipient( + uint256[] memory ids, + uint256[] memory amounts, + bytes memory mintData + ) public { + ERC1155Recipient to = new ERC1155Recipient(); + + uint256 minLength = min2(ids.length, amounts.length); + + uint256[] memory normalizedIds = new uint256[](minLength); + uint256[] memory normalizedAmounts = new uint256[](minLength); + + for (uint256 i = 0; i < minLength; i++) { + uint256 id = ids[i]; + + uint256 remainingMintAmountForId = type(uint256).max - userMintAmounts[address(to)][id]; + + uint256 mintAmount = bound(amounts[i], 0, remainingMintAmountForId); + + normalizedIds[i] = id; + normalizedAmounts[i] = mintAmount; + + userMintAmounts[address(to)][id] += mintAmount; + } + + token.batchMint(address(to), normalizedIds, normalizedAmounts, mintData); + + assertEq(to.batchOperator(), address(this)); + assertEq(to.batchFrom(), address(0)); + assertUintArrayEq(to.batchIds(), normalizedIds); + assertUintArrayEq(to.batchAmounts(), normalizedAmounts); + assertBytesEq(to.batchData(), mintData); + + for (uint256 i = 0; i < normalizedIds.length; i++) { + uint256 id = normalizedIds[i]; + + assertEq(token.balanceOf(address(to), id), userMintAmounts[address(to)][id]); + } + } + + function testBurn( + address to, + uint256 id, + uint256 mintAmount, + bytes memory mintData, + uint256 burnAmount + ) public { + if (to == address(0)) to = address(0xBEEF); + + if (uint256(uint160(to)) <= 18 || to.code.length > 0) return; + + burnAmount = bound(burnAmount, 0, mintAmount); + + token.mint(to, id, mintAmount, mintData); + + token.burn(to, id, burnAmount); + + assertEq(token.balanceOf(address(to), id), mintAmount - burnAmount); + } + + function testBatchBurn( + address to, + uint256[] memory ids, + uint256[] memory mintAmounts, + uint256[] memory burnAmounts, + bytes memory mintData + ) public { + if (to == address(0)) to = address(0xBEEF); + + if (uint256(uint160(to)) <= 18 || to.code.length > 0) return; + + uint256 minLength = min3(ids.length, mintAmounts.length, burnAmounts.length); + + uint256[] memory normalizedIds = new uint256[](minLength); + uint256[] memory normalizedMintAmounts = new uint256[](minLength); + uint256[] memory normalizedBurnAmounts = new uint256[](minLength); + + for (uint256 i = 0; i < minLength; i++) { + uint256 id = ids[i]; + + uint256 remainingMintAmountForId = type(uint256).max - userMintAmounts[address(to)][id]; + + normalizedIds[i] = id; + normalizedMintAmounts[i] = bound(mintAmounts[i], 0, remainingMintAmountForId); + normalizedBurnAmounts[i] = bound(burnAmounts[i], 0, normalizedMintAmounts[i]); + + userMintAmounts[address(to)][id] += normalizedMintAmounts[i]; + userTransferOrBurnAmounts[address(to)][id] += normalizedBurnAmounts[i]; + } + + token.batchMint(to, normalizedIds, normalizedMintAmounts, mintData); + + token.batchBurn(to, normalizedIds, normalizedBurnAmounts); + + for (uint256 i = 0; i < normalizedIds.length; i++) { + uint256 id = normalizedIds[i]; + + assertEq(token.balanceOf(to, id), userMintAmounts[to][id] - userTransferOrBurnAmounts[to][id]); + } + } + + function testApproveAll(address to, bool approved) public { + token.setApprovalForAll(to, approved); + + assertBoolEq(token.isApprovedForAll(address(this), to), approved); + } + + function testSafeTransferFromToEOA( + uint256 id, + uint256 mintAmount, + bytes memory mintData, + uint256 transferAmount, + address to, + bytes memory transferData + ) public { + if (to == address(0)) to = address(0xBEEF); + + if (uint256(uint160(to)) <= 18 || to.code.length > 0) return; + + transferAmount = bound(transferAmount, 0, mintAmount); + + ERC1155User from = new ERC1155User(token); + + token.mint(address(from), id, mintAmount, mintData); + + from.setApprovalForAll(address(this), true); + + token.safeTransferFrom(address(from), to, id, transferAmount, transferData); + + assertEq(token.balanceOf(to, id), transferAmount); + assertEq(token.balanceOf(address(from), id), mintAmount - transferAmount); + } + + function testSafeTransferFromToERC1155Recipient( + uint256 id, + uint256 mintAmount, + bytes memory mintData, + uint256 transferAmount, + bytes memory transferData + ) public { + ERC1155Recipient to = new ERC1155Recipient(); + + ERC1155User from = new ERC1155User(token); + + transferAmount = bound(transferAmount, 0, mintAmount); + + token.mint(address(from), id, mintAmount, mintData); + + from.setApprovalForAll(address(this), true); + + token.safeTransferFrom(address(from), address(to), id, transferAmount, transferData); + + assertEq(to.operator(), address(this)); + assertEq(to.from(), address(from)); + assertEq(to.id(), id); + assertBytesEq(to.mintData(), transferData); + + assertEq(token.balanceOf(address(to), id), transferAmount); + assertEq(token.balanceOf(address(from), id), mintAmount - transferAmount); + } + + function testSafeTransferFromSelf( + uint256 id, + uint256 mintAmount, + bytes memory mintData, + uint256 transferAmount, + address to, + bytes memory transferData + ) public { + if (to == address(0)) to = address(0xBEEF); + + if (uint256(uint160(to)) <= 18 || to.code.length > 0) return; + + transferAmount = bound(transferAmount, 0, mintAmount); + + token.mint(address(this), id, mintAmount, mintData); + + token.safeTransferFrom(address(this), to, id, transferAmount, transferData); + + assertEq(token.balanceOf(to, id), transferAmount); + assertEq(token.balanceOf(address(this), id), mintAmount - transferAmount); + } + + function testSafeBatchTransferFromToEOA( + address to, + uint256[] memory ids, + uint256[] memory mintAmounts, + uint256[] memory transferAmounts, + bytes memory mintData, + bytes memory transferData + ) public { + if (to == address(0)) to = address(0xBEEF); + + if (uint256(uint160(to)) <= 18 || to.code.length > 0) return; + + ERC1155User from = new ERC1155User(token); + + uint256 minLength = min3(ids.length, mintAmounts.length, transferAmounts.length); + + uint256[] memory normalizedIds = new uint256[](minLength); + uint256[] memory normalizedMintAmounts = new uint256[](minLength); + uint256[] memory normalizedTransferAmounts = new uint256[](minLength); + + for (uint256 i = 0; i < minLength; i++) { + uint256 id = ids[i]; + + uint256 remainingMintAmountForId = type(uint256).max - userMintAmounts[address(from)][id]; + + uint256 mintAmount = bound(mintAmounts[i], 0, remainingMintAmountForId); + uint256 transferAmount = bound(transferAmounts[i], 0, mintAmount); + + normalizedIds[i] = id; + normalizedMintAmounts[i] = mintAmount; + normalizedTransferAmounts[i] = transferAmount; + + userMintAmounts[address(from)][id] += mintAmount; + userTransferOrBurnAmounts[address(from)][id] += transferAmount; + } + + token.batchMint(address(from), normalizedIds, normalizedMintAmounts, mintData); + + from.setApprovalForAll(address(this), true); + + token.safeBatchTransferFrom(address(from), to, normalizedIds, normalizedTransferAmounts, transferData); + + for (uint256 i = 0; i < normalizedIds.length; i++) { + uint256 id = normalizedIds[i]; + + assertEq(token.balanceOf(address(to), id), userTransferOrBurnAmounts[address(from)][id]); + assertEq( + token.balanceOf(address(from), id), + userMintAmounts[address(from)][id] - userTransferOrBurnAmounts[address(from)][id] + ); + } + } + + function testSafeBatchTransferFromToERC1155Recipient( + uint256[] memory ids, + uint256[] memory mintAmounts, + uint256[] memory transferAmounts, + bytes memory mintData, + bytes memory transferData + ) public { + ERC1155User from = new ERC1155User(token); + + ERC1155Recipient to = new ERC1155Recipient(); + + uint256 minLength = min3(ids.length, mintAmounts.length, transferAmounts.length); + + uint256[] memory normalizedIds = new uint256[](minLength); + uint256[] memory normalizedMintAmounts = new uint256[](minLength); + uint256[] memory normalizedTransferAmounts = new uint256[](minLength); + + for (uint256 i = 0; i < minLength; i++) { + uint256 id = ids[i]; + + uint256 remainingMintAmountForId = type(uint256).max - userMintAmounts[address(from)][id]; + + uint256 mintAmount = bound(mintAmounts[i], 0, remainingMintAmountForId); + uint256 transferAmount = bound(transferAmounts[i], 0, mintAmount); + + normalizedIds[i] = id; + normalizedMintAmounts[i] = mintAmount; + normalizedTransferAmounts[i] = transferAmount; + + userMintAmounts[address(from)][id] += mintAmount; + userTransferOrBurnAmounts[address(from)][id] += transferAmount; + } + + token.batchMint(address(from), normalizedIds, normalizedMintAmounts, mintData); + + from.setApprovalForAll(address(this), true); + + token.safeBatchTransferFrom(address(from), address(to), normalizedIds, normalizedTransferAmounts, transferData); + + assertEq(to.batchOperator(), address(this)); + assertEq(to.batchFrom(), address(from)); + assertUintArrayEq(to.batchIds(), normalizedIds); + assertUintArrayEq(to.batchAmounts(), normalizedTransferAmounts); + assertBytesEq(to.batchData(), transferData); + + for (uint256 i = 0; i < normalizedIds.length; i++) { + uint256 id = normalizedIds[i]; + uint256 transferAmount = userTransferOrBurnAmounts[address(from)][id]; + + assertEq(token.balanceOf(address(to), id), transferAmount); + assertEq(token.balanceOf(address(from), id), userMintAmounts[address(from)][id] - transferAmount); + } + } + + function testBatchBalanceOf( + address[] memory tos, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory mintData + ) public { + uint256 minLength = min3(tos.length, ids.length, amounts.length); + + address[] memory normalizedTos = new address[](minLength); + uint256[] memory normalizedIds = new uint256[](minLength); + + for (uint256 i = 0; i < minLength; i++) { + uint256 id = ids[i]; + address to = tos[i] == address(0) ? address(0xBEEF) : tos[i]; + + uint256 remainingMintAmountForId = type(uint256).max - userMintAmounts[to][id]; + + normalizedTos[i] = to; + normalizedIds[i] = id; + + uint256 mintAmount = bound(amounts[i], 0, remainingMintAmountForId); + + token.mint(to, id, mintAmount, mintData); + + userMintAmounts[to][id] += mintAmount; + } + + uint256[] memory balances = token.balanceOfBatch(normalizedTos, normalizedIds); + + for (uint256 i = 0; i < normalizedTos.length; i++) { + assertEq(balances[i], token.balanceOf(normalizedTos[i], normalizedIds[i])); + } + } + + function testFailMintToZero( + uint256 id, + uint256 amount, + bytes memory data + ) public { + token.mint(address(0), id, amount, data); + } + + function testFailMintToNonERC155Recipient( + uint256 id, + uint256 mintAmount, + bytes memory mintData + ) public { + token.mint(address(new NonERC1155Recipient()), id, mintAmount, mintData); + } + + function testFailMintToRevertingERC155Recipient( + uint256 id, + uint256 mintAmount, + bytes memory mintData + ) public { + token.mint(address(new RevertingERC1155Recipient()), id, mintAmount, mintData); + } + + function testFailMintToWrongReturnDataERC155Recipient( + uint256 id, + uint256 mintAmount, + bytes memory mintData + ) public { + token.mint(address(new RevertingERC1155Recipient()), id, mintAmount, mintData); + } + + function testFailBurnInsufficientBalance( + address to, + uint256 id, + uint256 mintAmount, + uint256 burnAmount, + bytes memory mintData + ) public { + burnAmount = bound(burnAmount, mintAmount + 1, type(uint256).max); + + token.mint(to, id, mintAmount, mintData); + token.burn(to, id, burnAmount); + } + + function testFailSafeTransferFromInsufficientBalance( + address to, + uint256 id, + uint256 mintAmount, + uint256 transferAmount, + bytes memory mintData, + bytes memory transferData + ) public { + ERC1155User from = new ERC1155User(token); + + transferAmount = bound(transferAmount, mintAmount + 1, type(uint256).max); + + token.mint(address(from), id, mintAmount, mintData); + + from.setApprovalForAll(address(this), true); + + token.safeTransferFrom(address(from), to, id, transferAmount, transferData); + } + + function testFailSafeTransferFromSelfInsufficientBalance( + address to, + uint256 id, + uint256 mintAmount, + uint256 transferAmount, + bytes memory mintData, + bytes memory transferData + ) public { + transferAmount = bound(transferAmount, mintAmount + 1, type(uint256).max); + + token.mint(address(this), id, mintAmount, mintData); + token.safeTransferFrom(address(this), to, id, transferAmount, transferData); + } + + function testFailSafeTransferFromToZero( + uint256 id, + uint256 mintAmount, + uint256 transferAmount, + bytes memory mintData, + bytes memory transferData + ) public { + transferAmount = bound(transferAmount, 0, mintAmount); + + token.mint(address(this), id, mintAmount, mintData); + token.safeTransferFrom(address(this), address(0), id, transferAmount, transferData); + } + + function testFailSafeTransferFromToNonERC155Recipient( + uint256 id, + uint256 mintAmount, + uint256 transferAmount, + bytes memory mintData, + bytes memory transferData + ) public { + transferAmount = bound(transferAmount, 0, mintAmount); + + token.mint(address(this), id, mintAmount, mintData); + token.safeTransferFrom(address(this), address(new NonERC1155Recipient()), id, transferAmount, transferData); + } + + function testFailSafeTransferFromToRevertingERC1155Recipient( + uint256 id, + uint256 mintAmount, + uint256 transferAmount, + bytes memory mintData, + bytes memory transferData + ) public { + transferAmount = bound(transferAmount, 0, mintAmount); + + token.mint(address(this), id, mintAmount, mintData); + token.safeTransferFrom( + address(this), + address(new RevertingERC1155Recipient()), + id, + transferAmount, + transferData + ); + } + + function testFailSafeTransferFromToWrongReturnDataERC1155Recipient( + uint256 id, + uint256 mintAmount, + uint256 transferAmount, + bytes memory mintData, + bytes memory transferData + ) public { + transferAmount = bound(transferAmount, 0, mintAmount); + + token.mint(address(this), id, mintAmount, mintData); + token.safeTransferFrom( + address(this), + address(new WrongReturnDataERC1155Recipient()), + id, + transferAmount, + transferData + ); + } + + function testFailSafeBatchTransferInsufficientBalance( + address to, + uint256[] memory ids, + uint256[] memory mintAmounts, + uint256[] memory transferAmounts, + bytes memory mintData, + bytes memory transferData + ) public { + ERC1155User from = new ERC1155User(token); + + uint256 minLength = min3(ids.length, mintAmounts.length, transferAmounts.length); + + uint256[] memory normalizedIds = new uint256[](minLength); + uint256[] memory normalizedMintAmounts = new uint256[](minLength); + uint256[] memory normalizedTransferAmounts = new uint256[](minLength); + + for (uint256 i = 0; i < minLength; i++) { + uint256 id = ids[i]; + + uint256 remainingMintAmountForId = type(uint256).max - userMintAmounts[address(from)][id]; + + uint256 mintAmount = bound(mintAmounts[i], 0, remainingMintAmountForId); + uint256 transferAmount = bound(transferAmounts[i], mintAmount + 1, type(uint256).max); + + normalizedIds[i] = id; + normalizedMintAmounts[i] = mintAmount; + normalizedTransferAmounts[i] = transferAmount; + + userMintAmounts[address(from)][id] += mintAmount; + } + + token.batchMint(address(from), normalizedIds, normalizedMintAmounts, mintData); + + from.setApprovalForAll(address(this), true); + + token.safeBatchTransferFrom(address(from), to, normalizedIds, normalizedTransferAmounts, transferData); + } + + function testFailSafeBatchTransferFromToZero( + uint256[] memory ids, + uint256[] memory mintAmounts, + uint256[] memory transferAmounts, + bytes memory mintData, + bytes memory transferData + ) public { + ERC1155User from = new ERC1155User(token); + + uint256 minLength = min3(ids.length, mintAmounts.length, transferAmounts.length); + + uint256[] memory normalizedIds = new uint256[](minLength); + uint256[] memory normalizedMintAmounts = new uint256[](minLength); + uint256[] memory normalizedTransferAmounts = new uint256[](minLength); + + for (uint256 i = 0; i < minLength; i++) { + uint256 id = ids[i]; + + uint256 remainingMintAmountForId = type(uint256).max - userMintAmounts[address(from)][id]; + + uint256 mintAmount = bound(mintAmounts[i], 0, remainingMintAmountForId); + uint256 transferAmount = bound(transferAmounts[i], 0, mintAmount); + + normalizedIds[i] = id; + normalizedMintAmounts[i] = mintAmount; + normalizedTransferAmounts[i] = transferAmount; + + userMintAmounts[address(from)][id] += mintAmount; + } + + token.batchMint(address(from), normalizedIds, normalizedMintAmounts, mintData); + + from.setApprovalForAll(address(this), true); + + token.safeBatchTransferFrom(address(from), address(0), normalizedIds, normalizedTransferAmounts, transferData); + } + + function testFailSafeBatchTransferFromToNonERC1155Recipient( + uint256[] memory ids, + uint256[] memory mintAmounts, + uint256[] memory transferAmounts, + bytes memory mintData, + bytes memory transferData + ) public { + ERC1155User from = new ERC1155User(token); + + uint256 minLength = min3(ids.length, mintAmounts.length, transferAmounts.length); + + uint256[] memory normalizedIds = new uint256[](minLength); + uint256[] memory normalizedMintAmounts = new uint256[](minLength); + uint256[] memory normalizedTransferAmounts = new uint256[](minLength); + + for (uint256 i = 0; i < minLength; i++) { + uint256 id = ids[i]; + + uint256 remainingMintAmountForId = type(uint256).max - userMintAmounts[address(from)][id]; + + uint256 mintAmount = bound(mintAmounts[i], 0, remainingMintAmountForId); + uint256 transferAmount = bound(transferAmounts[i], 0, mintAmount); + + normalizedIds[i] = id; + normalizedMintAmounts[i] = mintAmount; + normalizedTransferAmounts[i] = transferAmount; + + userMintAmounts[address(from)][id] += mintAmount; + } + + token.batchMint(address(from), normalizedIds, normalizedMintAmounts, mintData); + + from.setApprovalForAll(address(this), true); + + token.safeBatchTransferFrom( + address(from), + address(new NonERC1155Recipient()), + normalizedIds, + normalizedTransferAmounts, + transferData + ); + } + + function testFailSafeBatchTransferFromToRevertingERC1155Recipient( + uint256[] memory ids, + uint256[] memory mintAmounts, + uint256[] memory transferAmounts, + bytes memory mintData, + bytes memory transferData + ) public { + ERC1155User from = new ERC1155User(token); + + uint256 minLength = min3(ids.length, mintAmounts.length, transferAmounts.length); + + uint256[] memory normalizedIds = new uint256[](minLength); + uint256[] memory normalizedMintAmounts = new uint256[](minLength); + uint256[] memory normalizedTransferAmounts = new uint256[](minLength); + + for (uint256 i = 0; i < minLength; i++) { + uint256 id = ids[i]; + + uint256 remainingMintAmountForId = type(uint256).max - userMintAmounts[address(from)][id]; + + uint256 mintAmount = bound(mintAmounts[i], 0, remainingMintAmountForId); + uint256 transferAmount = bound(transferAmounts[i], 0, mintAmount); + + normalizedIds[i] = id; + normalizedMintAmounts[i] = mintAmount; + normalizedTransferAmounts[i] = transferAmount; + + userMintAmounts[address(from)][id] += mintAmount; + } + + token.batchMint(address(from), normalizedIds, normalizedMintAmounts, mintData); + + from.setApprovalForAll(address(this), true); + + token.safeBatchTransferFrom( + address(from), + address(new RevertingERC1155Recipient()), + normalizedIds, + normalizedTransferAmounts, + transferData + ); + } + + function testFailSafeBatchTransferFromToWrongReturnDataERC1155Recipient( + uint256[] memory ids, + uint256[] memory mintAmounts, + uint256[] memory transferAmounts, + bytes memory mintData, + bytes memory transferData + ) public { + ERC1155User from = new ERC1155User(token); + + uint256 minLength = min3(ids.length, mintAmounts.length, transferAmounts.length); + + uint256[] memory normalizedIds = new uint256[](minLength); + uint256[] memory normalizedMintAmounts = new uint256[](minLength); + uint256[] memory normalizedTransferAmounts = new uint256[](minLength); + + for (uint256 i = 0; i < minLength; i++) { + uint256 id = ids[i]; + + uint256 remainingMintAmountForId = type(uint256).max - userMintAmounts[address(from)][id]; + + uint256 mintAmount = bound(mintAmounts[i], 0, remainingMintAmountForId); + uint256 transferAmount = bound(transferAmounts[i], 0, mintAmount); + + normalizedIds[i] = id; + normalizedMintAmounts[i] = mintAmount; + normalizedTransferAmounts[i] = transferAmount; + + userMintAmounts[address(from)][id] += mintAmount; + } + + token.batchMint(address(from), normalizedIds, normalizedMintAmounts, mintData); + + from.setApprovalForAll(address(this), true); + + token.safeBatchTransferFrom( + address(from), + address(new WrongReturnDataERC1155Recipient()), + normalizedIds, + normalizedTransferAmounts, + transferData + ); + } + + function testFailSafeBatchTransferFromWithArrayLengthMismatch( + address to, + uint256[] memory ids, + uint256[] memory mintAmounts, + uint256[] memory transferAmounts, + bytes memory mintData, + bytes memory transferData + ) public { + ERC1155User from = new ERC1155User(token); + + if (ids.length == transferAmounts.length) revert(); + + token.batchMint(address(from), ids, mintAmounts, mintData); + + from.setApprovalForAll(address(this), true); + + token.safeBatchTransferFrom(address(from), to, ids, transferAmounts, transferData); + } + + function testFailBatchMintToZero( + uint256[] memory ids, + uint256[] memory amounts, + bytes memory mintData + ) public { + uint256 minLength = min2(ids.length, amounts.length); + + uint256[] memory normalizedIds = new uint256[](minLength); + uint256[] memory normalizedAmounts = new uint256[](minLength); + + for (uint256 i = 0; i < minLength; i++) { + uint256 id = ids[i]; + + uint256 remainingMintAmountForId = type(uint256).max - userMintAmounts[address(0)][id]; + + uint256 mintAmount = bound(amounts[i], 0, remainingMintAmountForId); + + normalizedIds[i] = id; + normalizedAmounts[i] = mintAmount; + + userMintAmounts[address(0)][id] += mintAmount; + } + + token.batchMint(address(0), normalizedIds, normalizedAmounts, mintData); + } + + function testFailBatchMintToNonERC1155Recipient( + uint256[] memory ids, + uint256[] memory amounts, + bytes memory mintData + ) public { + NonERC1155Recipient to = new NonERC1155Recipient(); + + uint256 minLength = min2(ids.length, amounts.length); + + uint256[] memory normalizedIds = new uint256[](minLength); + uint256[] memory normalizedAmounts = new uint256[](minLength); + + for (uint256 i = 0; i < minLength; i++) { + uint256 id = ids[i]; + + uint256 remainingMintAmountForId = type(uint256).max - userMintAmounts[address(to)][id]; + + uint256 mintAmount = bound(amounts[i], 0, remainingMintAmountForId); + + normalizedIds[i] = id; + normalizedAmounts[i] = mintAmount; + + userMintAmounts[address(to)][id] += mintAmount; + } + + token.batchMint(address(to), normalizedIds, normalizedAmounts, mintData); + } + + function testFailBatchMintToRevertingERC1155Recipient( + uint256[] memory ids, + uint256[] memory amounts, + bytes memory mintData + ) public { + RevertingERC1155Recipient to = new RevertingERC1155Recipient(); + + uint256 minLength = min2(ids.length, amounts.length); + + uint256[] memory normalizedIds = new uint256[](minLength); + uint256[] memory normalizedAmounts = new uint256[](minLength); + + for (uint256 i = 0; i < minLength; i++) { + uint256 id = ids[i]; + + uint256 remainingMintAmountForId = type(uint256).max - userMintAmounts[address(to)][id]; + + uint256 mintAmount = bound(amounts[i], 0, remainingMintAmountForId); + + normalizedIds[i] = id; + normalizedAmounts[i] = mintAmount; + + userMintAmounts[address(to)][id] += mintAmount; + } + + token.batchMint(address(to), normalizedIds, normalizedAmounts, mintData); + } + + function testFailBatchMintToWrongReturnDataERC1155Recipient( + uint256[] memory ids, + uint256[] memory amounts, + bytes memory mintData + ) public { + WrongReturnDataERC1155Recipient to = new WrongReturnDataERC1155Recipient(); + + uint256 minLength = min2(ids.length, amounts.length); + + uint256[] memory normalizedIds = new uint256[](minLength); + uint256[] memory normalizedAmounts = new uint256[](minLength); + + for (uint256 i = 0; i < minLength; i++) { + uint256 id = ids[i]; + + uint256 remainingMintAmountForId = type(uint256).max - userMintAmounts[address(to)][id]; + + uint256 mintAmount = bound(amounts[i], 0, remainingMintAmountForId); + + normalizedIds[i] = id; + normalizedAmounts[i] = mintAmount; + + userMintAmounts[address(to)][id] += mintAmount; + } + + token.batchMint(address(to), normalizedIds, normalizedAmounts, mintData); + } + + function testFailBatchMintWithArrayMismatch( + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory mintData + ) public { + if (ids.length == amounts.length) revert(); + + token.batchMint(address(to), ids, amounts, mintData); + } + + function testFailBatchBurnInsufficientBalance( + address to, + uint256[] memory ids, + uint256[] memory mintAmounts, + uint256[] memory burnAmounts, + bytes memory mintData + ) public { + uint256 minLength = min3(ids.length, mintAmounts.length, burnAmounts.length); + + uint256[] memory normalizedIds = new uint256[](minLength); + uint256[] memory normalizedMintAmounts = new uint256[](minLength); + uint256[] memory normalizedBurnAmounts = new uint256[](minLength); + + for (uint256 i = 0; i < minLength; i++) { + uint256 id = ids[i]; + + uint256 remainingMintAmountForId = type(uint256).max - userMintAmounts[to][id]; + + normalizedIds[i] = id; + normalizedMintAmounts[i] = bound(mintAmounts[i], 0, remainingMintAmountForId); + normalizedBurnAmounts[i] = bound(burnAmounts[i], normalizedMintAmounts[i] + 1, type(uint256).max); + + userMintAmounts[to][id] += normalizedMintAmounts[i]; + } + + token.batchMint(to, normalizedIds, normalizedMintAmounts, mintData); + + token.batchBurn(to, normalizedIds, normalizedBurnAmounts); + } + + function testFailBatchBurnWithArrayLengthMismatch( + address to, + uint256[] memory ids, + uint256[] memory mintAmounts, + uint256[] memory burnAmounts, + bytes memory mintData + ) public { + if (ids.length == burnAmounts.length) revert(); + + token.batchMint(to, ids, mintAmounts, mintData); + + token.batchBurn(to, ids, burnAmounts); + } + + function testFailBalanceOfBatchWithArrayMismatch(address[] memory tos, uint256[] memory ids) public view { + if (tos.length == ids.length) revert(); + + token.balanceOfBatch(tos, ids); + } + + function onERC1155Received( + address, + address, + uint256, + uint256, + bytes calldata + ) public pure override returns (bytes4) { + return ERC1155TokenReceiver.onERC1155Received.selector; + } + + function onERC1155BatchReceived( + address, + address, + uint256[] calldata, + uint256[] calldata, + bytes calldata + ) external pure override returns (bytes4) { + return ERC1155TokenReceiver.onERC1155BatchReceived.selector; + } +} diff --git a/src/test/ERC20.t.sol b/src/test/ERC20.t.sol index 86d292d3..16b48b56 100644 --- a/src/test/ERC20.t.sol +++ b/src/test/ERC20.t.sol @@ -20,6 +20,190 @@ contract ERC20Test is DSTestPlus { assertEq(token.decimals(), 18); } + function testMetaData() public { + assertEq(token.name(), "Token"); + assertEq(token.symbol(), "TKN"); + assertEq(token.decimals(), 18); + } + + function testMint() public { + token.mint(address(0xBEEF), 1e18); + + assertEq(token.totalSupply(), 1e18); + assertEq(token.balanceOf(address(0xBEEF)), 1e18); + } + + function testBurn() public { + token.mint(address(0xBEEF), 1e18); + token.burn(address(0xBEEF), 0.9e18); + + assertEq(token.totalSupply(), 1e18 - 0.9e18); + assertEq(token.balanceOf(address(0xBEEF)), 0.1e18); + } + + function testApprove() public { + assertTrue(token.approve(address(0xBEEF), 1e18)); + + assertEq(token.allowance(address(this), address(0xBEEF)), 1e18); + } + + function testTransfer() public { + token.mint(address(this), 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 { + ERC20User from = new ERC20User(token); + + token.mint(address(from), 1e18); + + from.approve(address(this), 1e18); + + assertTrue(token.transferFrom(address(from), address(0xBEEF), 1e18)); + assertEq(token.totalSupply(), 1e18); + + assertEq(token.allowance(address(from), address(this)), 0); + + assertEq(token.balanceOf(address(from)), 0); + assertEq(token.balanceOf(address(0xBEEF)), 1e18); + } + + function testInfiniteApproveTransferFrom() public { + ERC20User from = new ERC20User(token); + + token.mint(address(from), 1e18); + + from.approve(address(this), type(uint256).max); + + assertTrue(token.transferFrom(address(from), address(0xBEEF), 1e18)); + assertEq(token.totalSupply(), 1e18); + + assertEq(token.allowance(address(from), address(this)), type(uint256).max); + + assertEq(token.balanceOf(address(from)), 0); + assertEq(token.balanceOf(address(0xBEEF)), 1e18); + } + + function testPermit() public { + uint256 privateKey = 0xBEEF; + address owner = hevm.addr(privateKey); + + (uint8 v, bytes32 r, bytes32 s) = hevm.sign( + privateKey, + keccak256( + abi.encodePacked( + "\x19\x01", + token.DOMAIN_SEPARATOR(), + keccak256(abi.encode(token.PERMIT_TYPEHASH(), owner, address(0xCAFE), 1e18, 0, block.timestamp)) + ) + ) + ); + + token.permit(owner, address(0xCAFE), 1e18, block.timestamp, v, r, s); + + assertEq(token.allowance(owner, address(0xCAFE)), 1e18); + assertEq(token.nonces(owner), 1); + } + + function testFailTransferInsufficientBalance() public { + token.mint(address(this), 0.9e18); + token.transfer(address(0xBEEF), 1e18); + } + + function testFailTransferFromInsufficientAllowance() public { + ERC20User from = new ERC20User(token); + + token.mint(address(from), 1e18); + from.approve(address(this), 0.9e18); + token.transferFrom(address(from), address(0xBEEF), 1e18); + } + + function testFailTransferFromInsufficientBalance() public { + ERC20User from = new ERC20User(token); + + token.mint(address(from), 0.9e18); + from.approve(address(this), 1e18); + token.transferFrom(address(from), address(0xBEEF), 1e18); + } + + function testFailPermitBadNonce() public { + uint256 privateKey = 0xBEEF; + address owner = hevm.addr(privateKey); + + (uint8 v, bytes32 r, bytes32 s) = hevm.sign( + privateKey, + keccak256( + abi.encodePacked( + "\x19\x01", + token.DOMAIN_SEPARATOR(), + keccak256(abi.encode(token.PERMIT_TYPEHASH(), owner, address(0xCAFE), 1e18, 1, block.timestamp)) + ) + ) + ); + + token.permit(owner, address(0xCAFE), 1e18, block.timestamp, v, r, s); + } + + function testFailPermitBadDeadline() public { + uint256 privateKey = 0xBEEF; + address owner = hevm.addr(privateKey); + + (uint8 v, bytes32 r, bytes32 s) = hevm.sign( + privateKey, + keccak256( + abi.encodePacked( + "\x19\x01", + token.DOMAIN_SEPARATOR(), + keccak256(abi.encode(token.PERMIT_TYPEHASH(), owner, address(0xCAFE), 1e18, 0, block.timestamp)) + ) + ) + ); + + token.permit(owner, address(0xCAFE), 1e18, block.timestamp + 1, v, r, s); + } + + function testFailPermitPastDeadline() public { + uint256 privateKey = 0xBEEF; + address owner = hevm.addr(privateKey); + + (uint8 v, bytes32 r, bytes32 s) = hevm.sign( + privateKey, + keccak256( + abi.encodePacked( + "\x19\x01", + token.DOMAIN_SEPARATOR(), + keccak256(abi.encode(token.PERMIT_TYPEHASH(), owner, address(0xCAFE), 1e18, 0, block.timestamp - 1)) + ) + ) + ); + + token.permit(owner, address(0xCAFE), 1e18, block.timestamp - 1, v, r, s); + } + + function testFailPermitReplay() public { + uint256 privateKey = 0xBEEF; + address owner = hevm.addr(privateKey); + + (uint8 v, bytes32 r, bytes32 s) = hevm.sign( + privateKey, + keccak256( + abi.encodePacked( + "\x19\x01", + token.DOMAIN_SEPARATOR(), + keccak256(abi.encode(token.PERMIT_TYPEHASH(), owner, address(0xCAFE), 1e18, 0, block.timestamp)) + ) + ) + ); + + token.permit(owner, address(0xCAFE), 1e18, block.timestamp, v, r, s); + token.permit(owner, address(0xCAFE), 1e18, block.timestamp, v, r, s); + } + function testMetaData( string calldata name, string calldata symbol, @@ -43,7 +227,7 @@ contract ERC20Test is DSTestPlus { uint256 mintAmount, uint256 burnAmount ) public { - if (burnAmount > mintAmount) return; + burnAmount = bound(burnAmount, 0, mintAmount); token.mint(from, mintAmount); token.burn(from, burnAmount); @@ -52,10 +236,10 @@ contract ERC20Test is DSTestPlus { assertEq(token.balanceOf(from), mintAmount - burnAmount); } - function testApprove(address from, uint256 amount) public { - assertTrue(token.approve(from, amount)); + function testApprove(address to, uint256 amount) public { + assertTrue(token.approve(to, amount)); - assertEq(token.allowance(address(this), from), amount); + assertEq(token.allowance(address(this), to), amount); } function testTransfer(address from, uint256 amount) public { @@ -77,7 +261,7 @@ contract ERC20Test is DSTestPlus { uint256 approval, uint256 amount ) public { - if (amount > approval) return; + amount = bound(amount, 0, approval); ERC20User from = new ERC20User(token); @@ -99,12 +283,62 @@ contract ERC20Test is DSTestPlus { } } + function testPermit( + uint256 privateKey, + address to, + uint256 amount, + uint256 deadline + ) public { + if (deadline < block.timestamp) deadline = block.timestamp; + if (privateKey == 0) privateKey = 1; + + address owner = hevm.addr(privateKey); + + (uint8 v, bytes32 r, bytes32 s) = hevm.sign( + privateKey, + keccak256( + abi.encodePacked( + "\x19\x01", + token.DOMAIN_SEPARATOR(), + keccak256(abi.encode(token.PERMIT_TYPEHASH(), owner, to, amount, 0, deadline)) + ) + ) + ); + + token.permit(owner, to, amount, deadline, v, r, s); + + assertEq(token.allowance(owner, to), amount); + assertEq(token.nonces(owner), 1); + } + + function testFailBurnInsufficientBalance( + address to, + uint256 mintAmount, + uint256 burnAmount + ) public { + burnAmount = bound(burnAmount, mintAmount + 1, type(uint256).max); + + token.mint(to, mintAmount); + token.burn(to, burnAmount); + } + + function testFailTransferInsufficientBalance( + address to, + uint256 mintAmount, + uint256 sendAmount + ) public { + sendAmount = bound(sendAmount, mintAmount + 1, type(uint256).max); + + token.mint(address(this), mintAmount); + token.transfer(to, sendAmount); + } + function testFailTransferFromInsufficientAllowance( address to, uint256 approval, uint256 amount ) public { - require(approval < amount); + amount = bound(amount, approval + 1, type(uint256).max); ERC20User from = new ERC20User(token); @@ -118,7 +352,7 @@ contract ERC20Test is DSTestPlus { uint256 mintAmount, uint256 sendAmount ) public { - require(mintAmount < sendAmount); + sendAmount = bound(sendAmount, mintAmount + 1, type(uint256).max); ERC20User from = new ERC20User(token); @@ -126,6 +360,109 @@ contract ERC20Test is DSTestPlus { from.approve(address(this), sendAmount); token.transferFrom(address(from), to, sendAmount); } + + function testFailPermitBadNonce( + uint256 privateKey, + address to, + uint256 amount, + uint256 deadline, + uint256 nonce + ) public { + if (deadline < block.timestamp) deadline = block.timestamp; + if (privateKey == 0) privateKey = 1; + if (nonce == 0) nonce = 1; + + address owner = hevm.addr(privateKey); + + (uint8 v, bytes32 r, bytes32 s) = hevm.sign( + privateKey, + keccak256( + abi.encodePacked( + "\x19\x01", + token.DOMAIN_SEPARATOR(), + keccak256(abi.encode(token.PERMIT_TYPEHASH(), owner, to, amount, nonce, deadline)) + ) + ) + ); + + token.permit(owner, to, amount, deadline, v, r, s); + } + + function testFailPermitBadDeadline( + uint256 privateKey, + address to, + uint256 amount, + uint256 deadline + ) public { + if (deadline < block.timestamp) deadline = block.timestamp; + if (privateKey == 0) privateKey = 1; + + address owner = hevm.addr(privateKey); + + (uint8 v, bytes32 r, bytes32 s) = hevm.sign( + privateKey, + keccak256( + abi.encodePacked( + "\x19\x01", + token.DOMAIN_SEPARATOR(), + keccak256(abi.encode(token.PERMIT_TYPEHASH(), owner, to, amount, 0, deadline)) + ) + ) + ); + + token.permit(owner, to, amount, deadline + 1, v, r, s); + } + + function testFailPermitPastDeadline( + uint256 privateKey, + address to, + uint256 amount, + uint256 deadline + ) public { + deadline = bound(deadline, 0, block.timestamp - 1); + if (privateKey == 0) privateKey = 1; + + address owner = hevm.addr(privateKey); + + (uint8 v, bytes32 r, bytes32 s) = hevm.sign( + privateKey, + keccak256( + abi.encodePacked( + "\x19\x01", + token.DOMAIN_SEPARATOR(), + keccak256(abi.encode(token.PERMIT_TYPEHASH(), owner, to, amount, 0, deadline)) + ) + ) + ); + + token.permit(owner, to, amount, deadline, v, r, s); + } + + function testFailPermitReplay( + uint256 privateKey, + address to, + uint256 amount, + uint256 deadline + ) public { + if (deadline < block.timestamp) deadline = block.timestamp; + if (privateKey == 0) privateKey = 1; + + address owner = hevm.addr(privateKey); + + (uint8 v, bytes32 r, bytes32 s) = hevm.sign( + privateKey, + keccak256( + abi.encodePacked( + "\x19\x01", + token.DOMAIN_SEPARATOR(), + keccak256(abi.encode(token.PERMIT_TYPEHASH(), owner, to, amount, 0, deadline)) + ) + ) + ); + + token.permit(owner, to, amount, deadline, v, r, s); + token.permit(owner, to, amount, deadline, v, r, s); + } } contract ERC20Invariants is DSTestPlus, DSInvariantTest { diff --git a/src/test/ERC721.t.sol b/src/test/ERC721.t.sol new file mode 100644 index 00000000..c8a71685 --- /dev/null +++ b/src/test/ERC721.t.sol @@ -0,0 +1,748 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.10; + +import {DSTestPlus} from "./utils/DSTestPlus.sol"; +import {DSInvariantTest} from "./utils/DSInvariantTest.sol"; + +import {MockERC721} from "./utils/mocks/MockERC721.sol"; +import {ERC721User} from "./utils/users/ERC721User.sol"; + +import {ERC721TokenReceiver} from "../tokens/ERC721.sol"; + +contract ERC721Recipient is ERC721TokenReceiver { + address public operator; + address public from; + uint256 public id; + bytes public data; + + function onERC721Received( + address _operator, + address _from, + uint256 _id, + bytes calldata _data + ) public virtual override returns (bytes4) { + operator = _operator; + from = _from; + id = _id; + data = _data; + + return ERC721TokenReceiver.onERC721Received.selector; + } +} + +contract RevertingERC721Recipient is ERC721TokenReceiver { + function onERC721Received( + address, + address, + uint256, + bytes calldata + ) public virtual override returns (bytes4) { + revert(string(abi.encodePacked(ERC721TokenReceiver.onERC721Received.selector))); + } +} + +contract WrongReturnDataERC721Recipient is ERC721TokenReceiver { + function onERC721Received( + address, + address, + uint256, + bytes calldata + ) public virtual override returns (bytes4) { + return 0xCAFEBEEF; + } +} + +contract NonERC721Recipient {} + +contract ERC721Test is DSTestPlus { + MockERC721 token; + + function setUp() public { + token = new MockERC721("Token", "TKN"); + } + + function invariantMetadata() public { + assertEq(token.name(), "Token"); + assertEq(token.symbol(), "TKN"); + } + + function testMetadata() public { + assertEq(token.name(), "Token"); + assertEq(token.symbol(), "TKN"); + } + + function testMint() public { + token.mint(address(0xBEEF), 1337); + + assertEq(token.totalSupply(), 1); + assertEq(token.balanceOf(address(0xBEEF)), 1); + assertEq(token.ownerOf(1337), address(0xBEEF)); + } + + function testBurn() public { + token.mint(address(0xBEEF), 1337); + token.burn(1337); + + assertEq(token.totalSupply(), 0); + assertEq(token.balanceOf(address(0xBEEF)), 0); + assertEq(token.ownerOf(1337), address(0)); + } + + function testApprove() public { + token.mint(address(this), 1337); + + token.approve(address(0xBEEF), 1337); + + assertEq(token.getApproved(1337), address(0xBEEF)); + } + + function testApproveAll() public { + token.setApprovalForAll(address(0xBEEF), true); + + assertTrue(token.isApprovedForAll(address(this), address(0xBEEF))); + } + + function testTransferFrom() public { + ERC721User from = new ERC721User(token); + + token.mint(address(from), 1337); + + from.approve(address(this), 1337); + + token.transferFrom(address(from), address(0xBEEF), 1337); + + assertEq(token.totalSupply(), 1); + assertEq(token.getApproved(1337), address(0)); + assertEq(token.ownerOf(1337), address(0xBEEF)); + assertEq(token.balanceOf(address(0xBEEF)), 1); + assertEq(token.balanceOf(address(from)), 0); + } + + function testTransferFromSelf() public { + token.mint(address(this), 1337); + + token.transferFrom(address(this), address(0xBEEF), 1337); + + assertEq(token.totalSupply(), 1); + assertEq(token.getApproved(1337), address(0)); + assertEq(token.ownerOf(1337), address(0xBEEF)); + assertEq(token.balanceOf(address(0xBEEF)), 1); + assertEq(token.balanceOf(address(this)), 0); + } + + function testTransferFromApproveAll() public { + ERC721User from = new ERC721User(token); + + token.mint(address(from), 1337); + + from.setApprovalForAll(address(this), true); + + token.transferFrom(address(from), address(0xBEEF), 1337); + + assertEq(token.totalSupply(), 1); + assertEq(token.getApproved(1337), address(0)); + assertEq(token.ownerOf(1337), address(0xBEEF)); + assertEq(token.balanceOf(address(0xBEEF)), 1); + assertEq(token.balanceOf(address(from)), 0); + } + + function testSafeTransferFromToEOA() public { + ERC721User from = new ERC721User(token); + + token.mint(address(from), 1337); + + from.setApprovalForAll(address(this), true); + + token.safeTransferFrom(address(from), address(0xBEEF), 1337); + + assertEq(token.totalSupply(), 1); + assertEq(token.getApproved(1337), address(0)); + assertEq(token.ownerOf(1337), address(0xBEEF)); + assertEq(token.balanceOf(address(0xBEEF)), 1); + assertEq(token.balanceOf(address(from)), 0); + } + + function testSafeTransferFromToERC721Recipient() public { + ERC721User from = new ERC721User(token); + ERC721Recipient recipient = new ERC721Recipient(); + + token.mint(address(from), 1337); + + from.setApprovalForAll(address(this), true); + + token.safeTransferFrom(address(from), address(recipient), 1337); + + assertEq(token.totalSupply(), 1); + assertEq(token.getApproved(1337), address(0)); + assertEq(token.ownerOf(1337), address(recipient)); + assertEq(token.balanceOf(address(recipient)), 1); + assertEq(token.balanceOf(address(from)), 0); + + assertEq(recipient.operator(), address(this)); + assertEq(recipient.from(), address(from)); + assertEq(recipient.id(), 1337); + assertBytesEq(recipient.data(), ""); + } + + function testSafeTransferFromToERC721RecipientWithData() public { + ERC721User from = new ERC721User(token); + ERC721Recipient recipient = new ERC721Recipient(); + + token.mint(address(from), 1337); + + from.setApprovalForAll(address(this), true); + + token.safeTransferFrom(address(from), address(recipient), 1337, "testing 123"); + + assertEq(token.totalSupply(), 1); + assertEq(token.getApproved(1337), address(0)); + assertEq(token.ownerOf(1337), address(recipient)); + assertEq(token.balanceOf(address(recipient)), 1); + assertEq(token.balanceOf(address(from)), 0); + + assertEq(recipient.operator(), address(this)); + assertEq(recipient.from(), address(from)); + assertEq(recipient.id(), 1337); + assertBytesEq(recipient.data(), "testing 123"); + } + + function testSafeMintToEOA() public { + token.safeMint(address(0xBEEF), 1337); + + assertEq(token.totalSupply(), 1); + assertEq(token.ownerOf(1337), address(address(0xBEEF))); + assertEq(token.balanceOf(address(address(0xBEEF))), 1); + } + + function testSafeMintToERC721Recipient() public { + ERC721Recipient to = new ERC721Recipient(); + + token.safeMint(address(to), 1337); + + assertEq(token.totalSupply(), 1); + assertEq(token.ownerOf(1337), address(to)); + assertEq(token.balanceOf(address(to)), 1); + + assertEq(to.operator(), address(this)); + assertEq(to.from(), address(0)); + assertEq(to.id(), 1337); + assertBytesEq(to.data(), ""); + } + + function testSafeMintToERC721RecipientWithData() public { + ERC721Recipient to = new ERC721Recipient(); + + token.safeMint(address(to), 1337, "testing 123"); + + assertEq(token.totalSupply(), 1); + assertEq(token.ownerOf(1337), address(to)); + assertEq(token.balanceOf(address(to)), 1); + + assertEq(to.operator(), address(this)); + assertEq(to.from(), address(0)); + assertEq(to.id(), 1337); + assertBytesEq(to.data(), "testing 123"); + } + + function testFailMintToZero() public { + token.mint(address(0), 1337); + } + + function testFailDoubleMint() public { + token.mint(address(0xBEEF), 1337); + token.mint(address(0xBEEF), 1337); + } + + function testFailBurnUnMinted() public { + token.burn(1337); + } + + function testFailDoubleBurn() public { + token.mint(address(0xBEEF), 1337); + + token.burn(1337); + token.burn(1337); + } + + function testFailApproveUnMinted() public { + token.approve(address(0xBEEF), 1337); + } + + function testFailApproveUnAuthorized() public { + token.mint(address(0xCAFE), 1337); + + token.approve(address(0xBEEF), 1337); + } + + function testFailTransferFromUnOwned() public { + token.transferFrom(address(0xFEED), address(0xBEEF), 1337); + } + + function testFailTransferFromWrongFrom() public { + token.mint(address(0xCAFE), 1337); + + token.transferFrom(address(0xFEED), address(0xBEEF), 1337); + } + + function testFailTransferFromToZero() public { + token.mint(address(this), 1337); + + token.transferFrom(address(this), address(0), 1337); + } + + function testFailTransferFromNotOwner() public { + token.mint(address(0xFEED), 1337); + + token.transferFrom(address(0xFEED), address(0xBEEF), 1337); + } + + function testFailSafeTransferFromToNonERC721Recipient() public { + token.mint(address(this), 1337); + + token.safeTransferFrom(address(this), address(new NonERC721Recipient()), 1337); + } + + function testFailSafeTransferFromToNonERC721RecipientWithData() public { + token.mint(address(this), 1337); + + token.safeTransferFrom(address(this), address(new NonERC721Recipient()), 1337, "testing 123"); + } + + function testFailSafeTransferFromToRevertingERC721Recipient() public { + token.mint(address(this), 1337); + + token.safeTransferFrom(address(this), address(new RevertingERC721Recipient()), 1337); + } + + function testFailSafeTransferFromToRevertingERC721RecipientWithData() public { + token.mint(address(this), 1337); + + token.safeTransferFrom(address(this), address(new RevertingERC721Recipient()), 1337, "testing 123"); + } + + function testFailSafeTransferFromToERC721RecipientWithWrongReturnData() public { + token.mint(address(this), 1337); + + token.safeTransferFrom(address(this), address(new WrongReturnDataERC721Recipient()), 1337); + } + + function testFailSafeTransferFromToERC721RecipientWithWrongReturnDataWithData() public { + token.mint(address(this), 1337); + + token.safeTransferFrom(address(this), address(new WrongReturnDataERC721Recipient()), 1337, "testing 123"); + } + + function testFailSafeMintToNonERC721Recipient() public { + token.safeMint(address(new NonERC721Recipient()), 1337); + } + + function testFailSafeMintToNonERC721RecipientWithData() public { + token.safeMint(address(new NonERC721Recipient()), 1337, "testing 123"); + } + + function testFailSafeMintToRevertingERC721Recipient() public { + token.safeMint(address(new RevertingERC721Recipient()), 1337); + } + + function testFailSafeMintToRevertingERC721RecipientWithData() public { + token.safeMint(address(new RevertingERC721Recipient()), 1337, "testing 123"); + } + + function testFailSafeMintToERC721RecipientWithWrongReturnData() public { + token.safeMint(address(new WrongReturnDataERC721Recipient()), 1337); + } + + function testFailSafeMintToERC721RecipientWithWrongReturnDataWithData() public { + token.safeMint(address(new WrongReturnDataERC721Recipient()), 1337, "testing 123"); + } + + function testMetadata(string memory name, string memory symbol) public { + MockERC721 tkn = new MockERC721(name, symbol); + + assertEq(tkn.name(), name); + assertEq(tkn.symbol(), symbol); + } + + function testMint(address to, uint256 id) public { + if (to == address(0)) to = address(0xBEEF); + + token.mint(to, id); + + assertEq(token.totalSupply(), 1); + assertEq(token.balanceOf(to), 1); + assertEq(token.ownerOf(id), to); + } + + function testBurn(address to, uint256 id) public { + if (to == address(0)) to = address(0xBEEF); + + token.mint(to, id); + token.burn(id); + + assertEq(token.totalSupply(), 0); + assertEq(token.balanceOf(to), 0); + assertEq(token.ownerOf(id), address(0)); + } + + function testApprove(address to, uint256 id) public { + if (to == address(0)) to = address(0xBEEF); + + token.mint(address(this), id); + + token.approve(to, id); + + assertEq(token.getApproved(id), to); + } + + function testApproveAll(address to, bool approved) public { + token.setApprovalForAll(to, approved); + + assertBoolEq(token.isApprovedForAll(address(this), to), approved); + } + + function testTransferFrom(uint256 id, address to) public { + if (to == address(0)) to = address(0xBEEF); + + ERC721User from = new ERC721User(token); + + token.mint(address(from), id); + + from.approve(address(this), id); + + token.transferFrom(address(from), to, id); + + assertEq(token.totalSupply(), 1); + assertEq(token.getApproved(id), address(0)); + assertEq(token.ownerOf(id), to); + assertEq(token.balanceOf(to), 1); + assertEq(token.balanceOf(address(from)), 0); + } + + function testTransferFromSelf(uint256 id, address to) public { + if (to == address(0)) to = address(0xBEEF); + + token.mint(address(this), id); + + token.transferFrom(address(this), to, id); + + assertEq(token.totalSupply(), 1); + assertEq(token.getApproved(id), address(0)); + assertEq(token.ownerOf(id), to); + assertEq(token.balanceOf(to), 1); + assertEq(token.balanceOf(address(this)), 0); + } + + function testTransferFromApproveAll(uint256 id, address to) public { + if (to == address(0)) to = address(0xBEEF); + + ERC721User from = new ERC721User(token); + + token.mint(address(from), id); + + from.setApprovalForAll(address(this), true); + + token.transferFrom(address(from), to, id); + + assertEq(token.totalSupply(), 1); + assertEq(token.getApproved(id), address(0)); + assertEq(token.ownerOf(id), to); + assertEq(token.balanceOf(to), 1); + assertEq(token.balanceOf(address(from)), 0); + } + + function testSafeTransferFromToEOA(uint256 id, address to) public { + if (to == address(0)) to = address(0xBEEF); + + if (uint256(uint160(to)) <= 18 || to.code.length > 0) return; + + ERC721User from = new ERC721User(token); + + token.mint(address(from), id); + + from.setApprovalForAll(address(this), true); + + token.safeTransferFrom(address(from), to, id); + + assertEq(token.totalSupply(), 1); + assertEq(token.getApproved(id), address(0)); + assertEq(token.ownerOf(id), to); + assertEq(token.balanceOf(to), 1); + assertEq(token.balanceOf(address(from)), 0); + } + + function testSafeTransferFromToERC721Recipient(uint256 id) public { + ERC721User from = new ERC721User(token); + ERC721Recipient recipient = new ERC721Recipient(); + + token.mint(address(from), id); + + from.setApprovalForAll(address(this), true); + + token.safeTransferFrom(address(from), address(recipient), id); + + assertEq(token.totalSupply(), 1); + assertEq(token.getApproved(id), address(0)); + assertEq(token.ownerOf(id), address(recipient)); + assertEq(token.balanceOf(address(recipient)), 1); + assertEq(token.balanceOf(address(from)), 0); + + assertEq(recipient.operator(), address(this)); + assertEq(recipient.from(), address(from)); + assertEq(recipient.id(), id); + assertBytesEq(recipient.data(), ""); + } + + function testSafeTransferFromToERC721RecipientWithData(uint256 id, bytes calldata data) public { + ERC721User from = new ERC721User(token); + ERC721Recipient recipient = new ERC721Recipient(); + + token.mint(address(from), id); + + from.setApprovalForAll(address(this), true); + + token.safeTransferFrom(address(from), address(recipient), id, data); + + assertEq(token.totalSupply(), 1); + assertEq(token.getApproved(id), address(0)); + assertEq(token.ownerOf(id), address(recipient)); + assertEq(token.balanceOf(address(recipient)), 1); + assertEq(token.balanceOf(address(from)), 0); + + assertEq(recipient.operator(), address(this)); + assertEq(recipient.from(), address(from)); + assertEq(recipient.id(), id); + assertBytesEq(recipient.data(), data); + } + + function testSafeMintToEOA(uint256 id, address to) public { + if (to == address(0)) to = address(0xBEEF); + + if (uint256(uint160(to)) <= 18 || to.code.length > 0) return; + + token.safeMint(to, id); + + assertEq(token.totalSupply(), 1); + assertEq(token.ownerOf(id), address(to)); + assertEq(token.balanceOf(address(to)), 1); + } + + function testSafeMintToERC721Recipient(uint256 id) public { + ERC721Recipient to = new ERC721Recipient(); + + token.safeMint(address(to), id); + + assertEq(token.totalSupply(), 1); + assertEq(token.ownerOf(id), address(to)); + assertEq(token.balanceOf(address(to)), 1); + + assertEq(to.operator(), address(this)); + assertEq(to.from(), address(0)); + assertEq(to.id(), id); + assertBytesEq(to.data(), ""); + } + + function testSafeMintToERC721RecipientWithData(uint256 id, bytes calldata data) public { + ERC721Recipient to = new ERC721Recipient(); + + token.safeMint(address(to), id, data); + + assertEq(token.totalSupply(), 1); + assertEq(token.ownerOf(id), address(to)); + assertEq(token.balanceOf(address(to)), 1); + + assertEq(to.operator(), address(this)); + assertEq(to.from(), address(0)); + assertEq(to.id(), id); + assertBytesEq(to.data(), data); + } + + function testFailMintToZero(uint256 id) public { + token.mint(address(0), id); + } + + function testFailDoubleMint(uint256 id, address to) public { + if (to == address(0)) to = address(0xBEEF); + + token.mint(to, id); + token.mint(to, id); + } + + function testFailBurnUnMinted(uint256 id) public { + token.burn(id); + } + + function testFailDoubleBurn(uint256 id, address to) public { + if (to == address(0)) to = address(0xBEEF); + + token.mint(to, id); + + token.burn(id); + token.burn(id); + } + + function testFailApproveUnMinted(uint256 id, address to) public { + token.approve(to, id); + } + + function testFailApproveUnAuthorized( + address owner, + uint256 id, + address to + ) public { + if (owner == address(0)) to = address(0xBEEF); + if (owner == address(this)) return; + + token.mint(owner, id); + + token.approve(to, id); + } + + function testFailTransferFromUnOwned( + address from, + address to, + uint256 id + ) public { + token.transferFrom(from, to, id); + } + + function testFailTransferFromWrongFrom( + address owner, + address from, + address to, + uint256 id + ) public { + if (owner == address(0)) to = address(0xBEEF); + if (from == owner) revert(); + + token.mint(owner, id); + + token.transferFrom(from, to, id); + } + + function testFailTransferFromToZero(uint256 id) public { + token.mint(address(this), id); + + token.transferFrom(address(this), address(0), id); + } + + function testFailTransferFromNotOwner( + address from, + address to, + uint256 id + ) public { + if (from == address(0)) to = address(0xBEEF); + + token.mint(from, id); + + token.transferFrom(from, to, id); + } + + function testFailSafeTransferFromToNonERC721Recipient(uint256 id) public { + token.mint(address(this), id); + + token.safeTransferFrom(address(this), address(new NonERC721Recipient()), id); + } + + function testFailSafeTransferFromToNonERC721RecipientWithData(uint256 id, bytes calldata data) public { + token.mint(address(this), id); + + token.safeTransferFrom(address(this), address(new NonERC721Recipient()), id, data); + } + + function testFailSafeTransferFromToRevertingERC721Recipient(uint256 id) public { + token.mint(address(this), id); + + token.safeTransferFrom(address(this), address(new RevertingERC721Recipient()), id); + } + + function testFailSafeTransferFromToRevertingERC721RecipientWithData(uint256 id, bytes calldata data) public { + token.mint(address(this), id); + + token.safeTransferFrom(address(this), address(new RevertingERC721Recipient()), id, data); + } + + function testFailSafeTransferFromToERC721RecipientWithWrongReturnData(uint256 id) public { + token.mint(address(this), id); + + token.safeTransferFrom(address(this), address(new WrongReturnDataERC721Recipient()), id); + } + + function testFailSafeTransferFromToERC721RecipientWithWrongReturnDataWithData(uint256 id, bytes calldata data) + public + { + token.mint(address(this), id); + + token.safeTransferFrom(address(this), address(new WrongReturnDataERC721Recipient()), id, data); + } + + function testFailSafeMintToNonERC721Recipient(uint256 id) public { + token.safeMint(address(new NonERC721Recipient()), id); + } + + function testFailSafeMintToNonERC721RecipientWithData(uint256 id, bytes calldata data) public { + token.safeMint(address(new NonERC721Recipient()), id, data); + } + + function testFailSafeMintToRevertingERC721Recipient(uint256 id) public { + token.safeMint(address(new RevertingERC721Recipient()), id); + } + + function testFailSafeMintToRevertingERC721RecipientWithData(uint256 id, bytes calldata data) public { + token.safeMint(address(new RevertingERC721Recipient()), id, data); + } + + function testFailSafeMintToERC721RecipientWithWrongReturnData(uint256 id) public { + token.safeMint(address(new WrongReturnDataERC721Recipient()), id); + } + + function testFailSafeMintToERC721RecipientWithWrongReturnDataWithData(uint256 id, bytes calldata data) public { + token.safeMint(address(new WrongReturnDataERC721Recipient()), id, data); + } +} + +contract ERC721Invariants is DSTestPlus, DSInvariantTest { + BalanceSum balanceSum; + MockERC721 token; + + function setUp() public { + token = new MockERC721("Token", "TKN"); + balanceSum = new BalanceSum(token); + + addTargetContract(address(balanceSum)); + } + + function invariantBalanceSum() public { + assertEq(token.totalSupply(), balanceSum.sum()); + } +} + +contract BalanceSum { + MockERC721 token; + uint256 public sum; + + constructor(MockERC721 _token) { + token = _token; + } + + function mint(address from, uint256 id) public { + token.mint(from, id); + sum++; + } + + function burn(uint256 id) public { + token.burn(id); + sum--; + } + + 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); + } +} diff --git a/src/test/FixedPointMathLib.t.sol b/src/test/FixedPointMathLib.t.sol index 16485649..da53e67a 100644 --- a/src/test/FixedPointMathLib.t.sol +++ b/src/test/FixedPointMathLib.t.sol @@ -49,33 +49,24 @@ contract FixedPointMathLibTest is DSTestPlus { } function testSqrt() public { + assertEq(FixedPointMathLib.sqrt(0), 0); + assertEq(FixedPointMathLib.sqrt(1), 1); assertEq(FixedPointMathLib.sqrt(2704), 52); assertEq(FixedPointMathLib.sqrt(110889), 333); assertEq(FixedPointMathLib.sqrt(32239684), 5678); } - function testMin() public { - assertEq(FixedPointMathLib.min(4, 100), 4); - assertEq(FixedPointMathLib.min(500, 400), 400); - assertEq(FixedPointMathLib.min(10000, 10001), 10000); - assertEq(FixedPointMathLib.min(1e18, 0.1e18), 0.1e18); - } - - function testMax() public { - assertEq(FixedPointMathLib.max(4, 100), 100); - assertEq(FixedPointMathLib.max(500, 400), 500); - assertEq(FixedPointMathLib.max(10000, 10001), 10001); - assertEq(FixedPointMathLib.max(1e18, 0.1e18), 1e18); - } - function testFMul( uint256 x, uint256 y, uint256 baseUnit ) public { - // Ignore cases where x * y overflows. + // Convert cases where x * y overflows into useful test cases. unchecked { - if (x != 0 && (x * y) / x != y) return; + while (x != 0 && (x * y) / x != y) { + x /= 2; + y /= 2; + } } assertEq(FixedPointMathLib.fmul(x, y, baseUnit), baseUnit == 0 ? 0 : (x * y) / baseUnit); @@ -99,16 +90,13 @@ contract FixedPointMathLibTest is DSTestPlus { uint256 y, uint256 baseUnit ) public { + if (y == 0) y = 1; + // Ignore cases where x * baseUnit overflows. unchecked { if (x != 0 && (x * baseUnit) / x != baseUnit) return; } - // Ignore cases where y is zero because it will cause a revert. - if (y == 0) { - return; - } - assertEq(FixedPointMathLib.fdiv(x, y, baseUnit), (x * baseUnit) / y); } @@ -140,20 +128,4 @@ contract FixedPointMathLibTest is DSTestPlus { assertTrue(root * root <= x && next * next > x); } - - function testMin(uint256 x, uint256 y) public { - if (x < y) { - assertEq(FixedPointMathLib.min(x, y), x); - } else { - assertEq(FixedPointMathLib.min(x, y), y); - } - } - - function testMax(uint256 x, uint256 y) public { - if (x > y) { - assertEq(FixedPointMathLib.max(x, y), x); - } else { - assertEq(FixedPointMathLib.max(x, y), y); - } - } } diff --git a/src/test/MultiRolesAuthority.t.sol b/src/test/MultiRolesAuthority.t.sol new file mode 100644 index 00000000..85308875 --- /dev/null +++ b/src/test/MultiRolesAuthority.t.sol @@ -0,0 +1,321 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.10; + +import {DSTestPlus} from "./utils/DSTestPlus.sol"; +import {MockAuthority} from "./utils/mocks/MockAuthority.sol"; + +import {Authority} from "../auth/Auth.sol"; + +import {MultiRolesAuthority} from "../auth/authorities/MultiRolesAuthority.sol"; + +contract MultiRolesAuthorityTest is DSTestPlus { + MultiRolesAuthority multiRolesAuthority; + + function setUp() public { + multiRolesAuthority = new MultiRolesAuthority(address(this), Authority(address(0))); + } + + function testSetRoles() public { + assertFalse(multiRolesAuthority.doesUserHaveRole(address(0xBEEF), 0)); + + multiRolesAuthority.setUserRole(address(0xBEEF), 0, true); + assertTrue(multiRolesAuthority.doesUserHaveRole(address(0xBEEF), 0)); + + multiRolesAuthority.setUserRole(address(0xBEEF), 0, false); + assertFalse(multiRolesAuthority.doesUserHaveRole(address(0xBEEF), 0)); + } + + function testSetRoleCapabilities() public { + assertFalse(multiRolesAuthority.doesRoleHaveCapability(0, 0xBEEFCAFE)); + + multiRolesAuthority.setRoleCapability(0, 0xBEEFCAFE, true); + assertTrue(multiRolesAuthority.doesRoleHaveCapability(0, 0xBEEFCAFE)); + + multiRolesAuthority.setRoleCapability(0, 0xBEEFCAFE, false); + assertFalse(multiRolesAuthority.doesRoleHaveCapability(0, 0xBEEFCAFE)); + } + + function testSetPublicCapabilities() public { + assertFalse(multiRolesAuthority.isCapabilityPublic(0xBEEFCAFE)); + + multiRolesAuthority.setPublicCapability(0xBEEFCAFE, true); + assertTrue(multiRolesAuthority.isCapabilityPublic(0xBEEFCAFE)); + + multiRolesAuthority.setPublicCapability(0xBEEFCAFE, false); + assertFalse(multiRolesAuthority.isCapabilityPublic(0xBEEFCAFE)); + } + + function testSetTargetCustomAuthority() public { + assertEq(address(multiRolesAuthority.getTargetCustomAuthority(address(0xBEEF))), address(0)); + + multiRolesAuthority.setTargetCustomAuthority(address(0xBEEF), Authority(address(0xCAFE))); + assertEq(address(multiRolesAuthority.getTargetCustomAuthority(address(0xBEEF))), address(0xCAFE)); + + multiRolesAuthority.setTargetCustomAuthority(address(0xBEEF), Authority(address(0))); + assertEq(address(multiRolesAuthority.getTargetCustomAuthority(address(0xBEEF))), address(0)); + } + + function testCanCallWithAuthorizedRole() public { + assertFalse(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setUserRole(address(0xBEEF), 0, true); + assertFalse(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setRoleCapability(0, 0xBEEFCAFE, true); + assertTrue(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setRoleCapability(0, 0xBEEFCAFE, false); + assertFalse(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setRoleCapability(0, 0xBEEFCAFE, true); + assertTrue(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setUserRole(address(0xBEEF), 0, false); + assertFalse(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + } + + function testCanCallPublicCapability() public { + assertFalse(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setPublicCapability(0xBEEFCAFE, true); + assertTrue(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setPublicCapability(0xBEEFCAFE, false); + assertFalse(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + } + + function testCanCallWithCustomAuthority() public { + assertFalse(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setTargetCustomAuthority(address(0xCAFE), new MockAuthority(false)); + assertFalse(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setTargetCustomAuthority(address(0xCAFE), new MockAuthority(true)); + assertTrue(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setTargetCustomAuthority(address(0xCAFE), new MockAuthority(false)); + assertFalse(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setTargetCustomAuthority(address(0xCAFE), Authority(address(0))); + assertFalse(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setTargetCustomAuthority(address(0xCAFE), new MockAuthority(true)); + assertTrue(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + } + + function testCanCallWithCustomAuthorityOverridesPublicCapability() public { + assertFalse(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setPublicCapability(0xBEEFCAFE, true); + assertTrue(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setTargetCustomAuthority(address(0xCAFE), new MockAuthority(false)); + assertFalse(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setTargetCustomAuthority(address(0xCAFE), new MockAuthority(true)); + assertTrue(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setPublicCapability(0xBEEFCAFE, false); + assertTrue(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setTargetCustomAuthority(address(0xCAFE), Authority(address(0))); + assertFalse(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setPublicCapability(0xBEEFCAFE, true); + assertTrue(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + } + + function testCanCallWithCustomAuthorityOverridesUserWithRole() public { + assertFalse(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setUserRole(address(0xBEEF), 0, true); + assertFalse(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setRoleCapability(0, 0xBEEFCAFE, true); + assertTrue(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setTargetCustomAuthority(address(0xCAFE), new MockAuthority(false)); + assertFalse(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setTargetCustomAuthority(address(0xCAFE), new MockAuthority(true)); + assertTrue(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setUserRole(address(0xBEEF), 0, false); + assertTrue(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setTargetCustomAuthority(address(0xCAFE), Authority(address(0))); + assertFalse(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setUserRole(address(0xBEEF), 0, true); + assertTrue(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setRoleCapability(0, 0xBEEFCAFE, false); + assertFalse(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setUserRole(address(0xBEEF), 0, false); + assertFalse(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + } + + function testSetRoles(address user, uint8 role) public { + assertFalse(multiRolesAuthority.doesUserHaveRole(user, role)); + + multiRolesAuthority.setUserRole(user, role, true); + assertTrue(multiRolesAuthority.doesUserHaveRole(user, role)); + + multiRolesAuthority.setUserRole(user, role, false); + assertFalse(multiRolesAuthority.doesUserHaveRole(user, role)); + } + + function testSetRoleCapabilities(uint8 role, bytes4 functionSig) public { + assertFalse(multiRolesAuthority.doesRoleHaveCapability(role, functionSig)); + + multiRolesAuthority.setRoleCapability(role, functionSig, true); + assertTrue(multiRolesAuthority.doesRoleHaveCapability(role, functionSig)); + + multiRolesAuthority.setRoleCapability(role, functionSig, false); + assertFalse(multiRolesAuthority.doesRoleHaveCapability(role, functionSig)); + } + + function testSetPublicCapabilities(bytes4 functionSig) public { + assertFalse(multiRolesAuthority.isCapabilityPublic(functionSig)); + + multiRolesAuthority.setPublicCapability(functionSig, true); + assertTrue(multiRolesAuthority.isCapabilityPublic(functionSig)); + + multiRolesAuthority.setPublicCapability(functionSig, false); + assertFalse(multiRolesAuthority.isCapabilityPublic(functionSig)); + } + + function testSetTargetCustomAuthority(address user, Authority customAuthority) public { + assertEq(address(multiRolesAuthority.getTargetCustomAuthority(user)), address(0)); + + multiRolesAuthority.setTargetCustomAuthority(user, customAuthority); + assertEq(address(multiRolesAuthority.getTargetCustomAuthority(user)), address(customAuthority)); + + multiRolesAuthority.setTargetCustomAuthority(user, Authority(address(0))); + assertEq(address(multiRolesAuthority.getTargetCustomAuthority(user)), address(0)); + } + + function testCanCallWithAuthorizedRole( + address user, + uint8 role, + address target, + bytes4 functionSig + ) public { + assertFalse(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setUserRole(user, role, true); + assertFalse(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setRoleCapability(role, functionSig, true); + assertTrue(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setRoleCapability(role, functionSig, false); + assertFalse(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setRoleCapability(role, functionSig, true); + assertTrue(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setUserRole(user, role, false); + assertFalse(multiRolesAuthority.canCall(user, target, functionSig)); + } + + function testCanCallPublicCapability( + address user, + address target, + bytes4 functionSig + ) public { + assertFalse(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setPublicCapability(functionSig, true); + assertTrue(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setPublicCapability(functionSig, false); + assertFalse(multiRolesAuthority.canCall(user, target, functionSig)); + } + + function testCanCallWithCustomAuthority( + address user, + address target, + bytes4 functionSig + ) public { + assertFalse(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setTargetCustomAuthority(target, new MockAuthority(false)); + assertFalse(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setTargetCustomAuthority(target, new MockAuthority(true)); + assertTrue(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setTargetCustomAuthority(target, new MockAuthority(false)); + assertFalse(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setTargetCustomAuthority(target, Authority(address(0))); + assertFalse(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setTargetCustomAuthority(target, new MockAuthority(true)); + assertTrue(multiRolesAuthority.canCall(user, target, functionSig)); + } + + function testCanCallWithCustomAuthorityOverridesPublicCapability( + address user, + address target, + bytes4 functionSig + ) public { + assertFalse(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setPublicCapability(functionSig, true); + assertTrue(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setTargetCustomAuthority(target, new MockAuthority(false)); + assertFalse(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setTargetCustomAuthority(target, new MockAuthority(true)); + assertTrue(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setPublicCapability(functionSig, false); + assertTrue(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setTargetCustomAuthority(target, Authority(address(0))); + assertFalse(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setPublicCapability(functionSig, true); + assertTrue(multiRolesAuthority.canCall(user, target, functionSig)); + } + + function testCanCallWithCustomAuthorityOverridesUserWithRole( + address user, + uint8 role, + address target, + bytes4 functionSig + ) public { + assertFalse(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setUserRole(user, role, true); + assertFalse(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setRoleCapability(role, functionSig, true); + assertTrue(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setTargetCustomAuthority(target, new MockAuthority(false)); + assertFalse(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setTargetCustomAuthority(target, new MockAuthority(true)); + assertTrue(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setUserRole(user, role, false); + assertTrue(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setTargetCustomAuthority(target, Authority(address(0))); + assertFalse(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setUserRole(user, role, true); + assertTrue(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setRoleCapability(role, functionSig, false); + assertFalse(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setUserRole(user, role, false); + assertFalse(multiRolesAuthority.canCall(user, target, functionSig)); + } +} diff --git a/src/test/RolesAuthority.t.sol b/src/test/RolesAuthority.t.sol index 2ff335dc..88c43fcc 100644 --- a/src/test/RolesAuthority.t.sol +++ b/src/test/RolesAuthority.t.sol @@ -2,97 +2,147 @@ pragma solidity 0.8.10; import {DSTestPlus} from "./utils/DSTestPlus.sol"; -import {MockAuthChild} from "./utils/mocks/MockAuthChild.sol"; +import {MockAuthority} from "./utils/mocks/MockAuthority.sol"; + +import {Authority} from "../auth/Auth.sol"; -import {Auth, Authority} from "../auth/Auth.sol"; import {RolesAuthority} from "../auth/authorities/RolesAuthority.sol"; contract RolesAuthorityTest is DSTestPlus { - RolesAuthority roles; - MockAuthChild mockAuthChild; + RolesAuthority rolesAuthority; function setUp() public { - roles = new RolesAuthority(address(this), Authority(address(0))); - mockAuthChild = new MockAuthChild(); + rolesAuthority = new RolesAuthority(address(this), Authority(address(0))); + } + + function testSetRoles() public { + assertFalse(rolesAuthority.doesUserHaveRole(address(0xBEEF), 0)); - mockAuthChild.setAuthority(roles); - mockAuthChild.setOwner(DEAD_ADDRESS); + rolesAuthority.setUserRole(address(0xBEEF), 0, true); + assertTrue(rolesAuthority.doesUserHaveRole(address(0xBEEF), 0)); + + rolesAuthority.setUserRole(address(0xBEEF), 0, false); + assertFalse(rolesAuthority.doesUserHaveRole(address(0xBEEF), 0)); } - function invariantOwner() public { - assertEq(roles.owner(), address(this)); - assertEq(mockAuthChild.owner(), DEAD_ADDRESS); + function testSetRoleCapabilities() public { + assertFalse(rolesAuthority.doesRoleHaveCapability(0, address(0xCAFE), 0xBEEFCAFE)); + + rolesAuthority.setRoleCapability(0, address(0xCAFE), 0xBEEFCAFE, true); + assertTrue(rolesAuthority.doesRoleHaveCapability(0, address(0xCAFE), 0xBEEFCAFE)); + + rolesAuthority.setRoleCapability(0, address(0xCAFE), 0xBEEFCAFE, false); + assertFalse(rolesAuthority.doesRoleHaveCapability(0, address(0xCAFE), 0xBEEFCAFE)); } - function invariantAuthority() public { - assertEq(address(roles.authority()), address(0)); - assertEq(address(mockAuthChild.authority()), address(roles)); + function testSetPublicCapabilities() public { + assertFalse(rolesAuthority.isCapabilityPublic(address(0xCAFE), 0xBEEFCAFE)); + + rolesAuthority.setPublicCapability(address(0xCAFE), 0xBEEFCAFE, true); + assertTrue(rolesAuthority.isCapabilityPublic(address(0xCAFE), 0xBEEFCAFE)); + + rolesAuthority.setPublicCapability(address(0xCAFE), 0xBEEFCAFE, false); + assertFalse(rolesAuthority.isCapabilityPublic(address(0xCAFE), 0xBEEFCAFE)); + } + + function testCanCallWithAuthorizedRole() public { + assertFalse(rolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + rolesAuthority.setUserRole(address(0xBEEF), 0, true); + assertFalse(rolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + rolesAuthority.setRoleCapability(0, address(0xCAFE), 0xBEEFCAFE, true); + assertTrue(rolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + rolesAuthority.setRoleCapability(0, address(0xCAFE), 0xBEEFCAFE, false); + assertFalse(rolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + rolesAuthority.setRoleCapability(0, address(0xCAFE), 0xBEEFCAFE, true); + assertTrue(rolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + rolesAuthority.setUserRole(address(0xBEEF), 0, false); + assertFalse(rolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); } - function testSanityChecks() public { - assertEq(roles.getUserRoles(address(this)), bytes32(0)); - assertFalse(roles.isUserRoot(address(this))); - assertFalse(roles.canCall(address(this), address(mockAuthChild), MockAuthChild.updateFlag.selector)); + function testCanCallPublicCapability() public { + assertFalse(rolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + rolesAuthority.setPublicCapability(address(0xCAFE), 0xBEEFCAFE, true); + assertTrue(rolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); - try mockAuthChild.updateFlag() { - fail("Trust Authority Allowed Attacker To Update Flag"); - } catch {} + rolesAuthority.setPublicCapability(address(0xCAFE), 0xBEEFCAFE, false); + assertFalse(rolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); } - function testBasics() public { - uint8 rootRole = 0; - uint8 adminRole = 1; - uint8 modRole = 2; - uint8 userRole = 3; + function testSetRoles(address user, uint8 role) public { + assertFalse(rolesAuthority.doesUserHaveRole(user, role)); - roles.setUserRole(address(this), rootRole, true); - roles.setUserRole(address(this), adminRole, true); + rolesAuthority.setUserRole(user, role, true); + assertTrue(rolesAuthority.doesUserHaveRole(user, role)); - assertEq32( - 0x0000000000000000000000000000000000000000000000000000000000000003, - roles.getUserRoles(address(this)) - ); + rolesAuthority.setUserRole(user, role, false); + assertFalse(rolesAuthority.doesUserHaveRole(user, role)); + } - roles.setRoleCapability(adminRole, address(mockAuthChild), MockAuthChild.updateFlag.selector, true); - assertTrue(roles.doesRoleHaveCapability(adminRole, address(mockAuthChild), MockAuthChild.updateFlag.selector)); - assertTrue(roles.canCall(address(this), address(mockAuthChild), MockAuthChild.updateFlag.selector)); + function testSetRoleCapabilities( + uint8 role, + address target, + bytes4 functionSig + ) public { + assertFalse(rolesAuthority.doesRoleHaveCapability(role, target, functionSig)); - mockAuthChild.updateFlag(); + rolesAuthority.setRoleCapability(role, target, functionSig, true); + assertTrue(rolesAuthority.doesRoleHaveCapability(role, target, functionSig)); - roles.setRoleCapability(adminRole, address(mockAuthChild), MockAuthChild.updateFlag.selector, false); - assertFalse(roles.doesRoleHaveCapability(adminRole, address(mockAuthChild), MockAuthChild.updateFlag.selector)); - assertFalse(roles.canCall(address(this), address(mockAuthChild), MockAuthChild.updateFlag.selector)); + rolesAuthority.setRoleCapability(role, target, functionSig, false); + assertFalse(rolesAuthority.doesRoleHaveCapability(role, target, functionSig)); + } + + function testSetPublicCapabilities(address target, bytes4 functionSig) public { + assertFalse(rolesAuthority.isCapabilityPublic(target, functionSig)); - assertTrue(roles.doesUserHaveRole(address(this), rootRole)); - assertTrue(roles.doesUserHaveRole(address(this), adminRole)); + rolesAuthority.setPublicCapability(target, functionSig, true); + assertTrue(rolesAuthority.isCapabilityPublic(target, functionSig)); - assertFalse(roles.doesUserHaveRole(address(this), modRole)); - assertFalse(roles.doesUserHaveRole(address(this), userRole)); + rolesAuthority.setPublicCapability(target, functionSig, false); + assertFalse(rolesAuthority.isCapabilityPublic(target, functionSig)); } - function testRoot() public { - assertFalse(roles.isUserRoot(address(this))); - assertFalse(roles.canCall(address(this), address(mockAuthChild), MockAuthChild.updateFlag.selector)); + function testCanCallWithAuthorizedRole( + address user, + uint8 role, + address target, + bytes4 functionSig + ) public { + assertFalse(rolesAuthority.canCall(user, target, functionSig)); + + rolesAuthority.setUserRole(user, role, true); + assertFalse(rolesAuthority.canCall(user, target, functionSig)); + + rolesAuthority.setRoleCapability(role, target, functionSig, true); + assertTrue(rolesAuthority.canCall(user, target, functionSig)); + + rolesAuthority.setRoleCapability(role, target, functionSig, false); + assertFalse(rolesAuthority.canCall(user, target, functionSig)); - roles.setRootUser(address(this), true); - assertTrue(roles.isUserRoot(address(this))); - assertTrue(roles.canCall(address(this), address(mockAuthChild), MockAuthChild.updateFlag.selector)); + rolesAuthority.setRoleCapability(role, target, functionSig, true); + assertTrue(rolesAuthority.canCall(user, target, functionSig)); - roles.setRootUser(address(this), false); - assertFalse(roles.isUserRoot(address(this))); - assertFalse(roles.canCall(address(this), address(mockAuthChild), MockAuthChild.updateFlag.selector)); + rolesAuthority.setUserRole(user, role, false); + assertFalse(rolesAuthority.canCall(user, target, functionSig)); } - function testPublicCapabilities() public { - assertFalse(roles.isCapabilityPublic(address(mockAuthChild), MockAuthChild.updateFlag.selector)); - assertFalse(roles.canCall(address(this), address(mockAuthChild), MockAuthChild.updateFlag.selector)); + function testCanCallPublicCapability( + address user, + address target, + bytes4 functionSig + ) public { + assertFalse(rolesAuthority.canCall(user, target, functionSig)); - roles.setPublicCapability(address(mockAuthChild), MockAuthChild.updateFlag.selector, true); - assertTrue(roles.isCapabilityPublic(address(mockAuthChild), MockAuthChild.updateFlag.selector)); - assertTrue(roles.canCall(address(this), address(mockAuthChild), MockAuthChild.updateFlag.selector)); + rolesAuthority.setPublicCapability(target, functionSig, true); + assertTrue(rolesAuthority.canCall(user, target, functionSig)); - roles.setPublicCapability(address(mockAuthChild), MockAuthChild.updateFlag.selector, false); - assertFalse(roles.isCapabilityPublic(address(mockAuthChild), MockAuthChild.updateFlag.selector)); - assertFalse(roles.canCall(address(this), address(mockAuthChild), MockAuthChild.updateFlag.selector)); + rolesAuthority.setPublicCapability(target, functionSig, false); + assertFalse(rolesAuthority.canCall(user, target, functionSig)); } } diff --git a/src/test/SSTORE2.t.sol b/src/test/SSTORE2.t.sol index ce454d07..8b0cb496 100644 --- a/src/test/SSTORE2.t.sol +++ b/src/test/SSTORE2.t.sol @@ -67,7 +67,7 @@ contract SSTORE2Test is DSTestPlus { function testWriteReadCustomStartBound(bytes calldata testBytes, uint256 startIndex) public { if (testBytes.length == 0) return; - startIndex %= testBytes.length; + startIndex = bound(startIndex, 0, testBytes.length); assertBytesEq(SSTORE2.read(SSTORE2.write(testBytes), startIndex), bytes(testBytes[startIndex:])); } @@ -79,8 +79,8 @@ contract SSTORE2Test is DSTestPlus { ) public { if (testBytes.length == 0) return; - startIndex %= testBytes.length; - endIndex %= testBytes.length; + endIndex = bound(endIndex, 0, testBytes.length); + startIndex = bound(startIndex, 0, testBytes.length); if (startIndex > endIndex) return; @@ -107,7 +107,7 @@ contract SSTORE2Test is DSTestPlus { } function testFailWriteReadCustomStartBoundOutOfRange(bytes calldata testBytes, uint256 startIndex) public { - if (testBytes.length >= startIndex) revert(); + startIndex = bound(startIndex, testBytes.length + 1, type(uint256).max); SSTORE2.read(SSTORE2.write(testBytes), startIndex); } @@ -117,7 +117,7 @@ contract SSTORE2Test is DSTestPlus { uint256 startIndex, uint256 endIndex ) public { - if (endIndex >= startIndex) revert(); + endIndex = bound(endIndex, startIndex + 1, type(uint256).max); SSTORE2.read(SSTORE2.write(testBytes), startIndex, endIndex); } diff --git a/src/test/SafeCastLib.t.sol b/src/test/SafeCastLib.t.sol index b6f78591..ccce1d32 100644 --- a/src/test/SafeCastLib.t.sol +++ b/src/test/SafeCastLib.t.sol @@ -52,61 +52,61 @@ contract SafeCastLibTest is DSTestPlus { } function testSafeCastTo248(uint256 x) public { - x %= type(uint248).max; + x = bound(x, 0, type(uint248).max); assertEq(SafeCastLib.safeCastTo248(x), x); } function testSafeCastTo128(uint256 x) public { - x %= type(uint128).max; + x = bound(x, 0, type(uint128).max); assertEq(SafeCastLib.safeCastTo128(x), x); } function testSafeCastTo96(uint256 x) public { - x %= type(uint96).max; + x = bound(x, 0, type(uint96).max); assertEq(SafeCastLib.safeCastTo96(x), x); } function testSafeCastTo64(uint256 x) public { - x %= type(uint64).max; + x = bound(x, 0, type(uint64).max); assertEq(SafeCastLib.safeCastTo64(x), x); } function testSafeCastTo32(uint256 x) public { - x %= type(uint32).max; + x = bound(x, 0, type(uint32).max); assertEq(SafeCastLib.safeCastTo32(x), x); } function testFailSafeCastTo248(uint256 x) public pure { - if (type(uint248).max > x) revert(); + x = bound(x, type(uint248).max + 1, type(uint256).max); SafeCastLib.safeCastTo248(x); } function testFailSafeCastTo128(uint256 x) public pure { - if (type(uint128).max > x) revert(); + x = bound(x, type(uint128).max + 1, type(uint256).max); SafeCastLib.safeCastTo128(x); } function testFailSafeCastTo96(uint256 x) public pure { - if (type(uint96).max > x) revert(); + x = bound(x, type(uint96).max + 1, type(uint256).max); SafeCastLib.safeCastTo96(x); } function testFailSafeCastTo64(uint256 x) public pure { - if (type(uint64).max > x) revert(); + x = bound(x, type(uint64).max + 1, type(uint256).max); SafeCastLib.safeCastTo64(x); } function testFailSafeCastTo32(uint256 x) public pure { - if (type(uint32).max > x) revert(); + x = bound(x, type(uint32).max + 1, type(uint256).max); SafeCastLib.safeCastTo32(x); } diff --git a/src/test/SafeTransferLib.t.sol b/src/test/SafeTransferLib.t.sol index d691044c..1e570b2f 100644 --- a/src/test/SafeTransferLib.t.sol +++ b/src/test/SafeTransferLib.t.sol @@ -127,9 +127,7 @@ contract SafeTransferLibTest is DSTestPlus { address to, uint256 amount ) public { - if (nonContract.code.length > 0) return; - - if (uint256(uint160(nonContract)) <= 18) return; // Some precompiles cause reverts. + if (uint256(uint160(nonContract)) <= 18 || nonContract.code.length > 0) return; SafeTransferLib.safeTransfer(SolmateERC20(nonContract), to, amount); } @@ -164,9 +162,7 @@ contract SafeTransferLibTest is DSTestPlus { address to, uint256 amount ) public { - if (nonContract.code.length > 0) return; - - if (uint256(uint160(nonContract)) <= 18) return; // Some precompiles cause reverts. + if (uint256(uint160(nonContract)) <= 18 || nonContract.code.length > 0) return; SafeTransferLib.safeTransferFrom(SolmateERC20(nonContract), from, to, amount); } @@ -188,17 +184,15 @@ contract SafeTransferLibTest is DSTestPlus { address to, uint256 amount ) public { - if (nonContract.code.length > 0) return; - - if (uint256(uint160(nonContract)) <= 18) return; // Some precompiles cause reverts. + if (uint256(uint160(nonContract)) <= 18 || nonContract.code.length > 0) return; SafeTransferLib.safeApprove(SolmateERC20(nonContract), to, amount); } function testTransferETH(address recipient, uint256 amount) public { - if (uint256(uint160(recipient)) <= 18) return; // Some precompiles cause reverts. + if (uint256(uint160(recipient)) <= 18) return; - amount %= address(this).balance; + amount = bound(amount, 0, address(this).balance); SafeTransferLib.safeTransferETH(recipient, amount); } diff --git a/src/test/Trust.t.sol b/src/test/Trust.t.sol deleted file mode 100644 index 76048864..00000000 --- a/src/test/Trust.t.sol +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity 0.8.10; - -import {DSTestPlus} from "./utils/DSTestPlus.sol"; -import {MockTrustChild} from "./utils/mocks/MockTrustChild.sol"; - -contract TrustTest is DSTestPlus { - MockTrustChild mockTrustChild; - - function setUp() public { - mockTrustChild = new MockTrustChild(); - - mockTrustChild.setIsTrusted(address(this), false); - } - - function testFailTrustNotTrusted(address usr) public { - mockTrustChild.setIsTrusted(usr, true); - } - - function testFailDistrustNotTrusted(address usr) public { - mockTrustChild.setIsTrusted(usr, false); - } - - function testTrust(address usr) public { - if (usr == address(this)) return; - forceTrust(address(this)); - - assertFalse(mockTrustChild.isTrusted(usr)); - mockTrustChild.setIsTrusted(usr, true); - assertTrue(mockTrustChild.isTrusted(usr)); - } - - function testDistrust(address usr) public { - if (usr == address(this)) return; - forceTrust(address(this)); - forceTrust(usr); - - assertTrue(mockTrustChild.isTrusted(usr)); - mockTrustChild.setIsTrusted(usr, false); - assertFalse(mockTrustChild.isTrusted(usr)); - } - - function forceTrust(address usr) internal { - hevm.store(address(mockTrustChild), keccak256(abi.encode(usr, uint256(0))), bytes32(uint256(1))); - } -} diff --git a/src/test/TrustAuthority.t.sol b/src/test/TrustAuthority.t.sol deleted file mode 100644 index 1bf54c21..00000000 --- a/src/test/TrustAuthority.t.sol +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity 0.8.10; - -import {DSTestPlus} from "./utils/DSTestPlus.sol"; -import {MockAuthChild} from "./utils/mocks/MockAuthChild.sol"; - -import {TrustAuthority} from "../auth/authorities/TrustAuthority.sol"; - -contract TrustAuthorityTest is DSTestPlus { - TrustAuthority trust; - MockAuthChild mockAuthChild; - - function setUp() public { - trust = new TrustAuthority(address(this)); - mockAuthChild = new MockAuthChild(); - - mockAuthChild.setAuthority(trust); - mockAuthChild.setOwner(DEAD_ADDRESS); - - trust.setIsTrusted(address(this), false); - } - - function invariantOwner() public { - assertEq(mockAuthChild.owner(), DEAD_ADDRESS); - } - - function invariantAuthority() public { - assertEq(address(mockAuthChild.authority()), address(trust)); - } - - function testSanityChecks() public { - assertFalse(trust.isTrusted(address(this))); - assertFalse(trust.canCall(address(this), address(mockAuthChild), MockAuthChild.updateFlag.selector)); - try mockAuthChild.updateFlag() { - fail("Trust Authority Let Attacker Update Flag"); - } catch {} - } - - function testUpdateTrust() public { - forceTrust(address(this)); - assertTrue(trust.isTrusted(address(this))); - assertTrue(trust.canCall(address(this), address(mockAuthChild), MockAuthChild.updateFlag.selector)); - mockAuthChild.updateFlag(); - - trust.setIsTrusted(address(this), false); - assertFalse(trust.isTrusted(address(this))); - assertFalse(trust.canCall(address(this), address(mockAuthChild), MockAuthChild.updateFlag.selector)); - try mockAuthChild.updateFlag() { - fail("Trust Authority Allowed Attacker To Update Flag"); - } catch {} - } - - function forceTrust(address usr) internal { - hevm.store(address(trust), keccak256(abi.encode(usr, uint256(0))), bytes32(uint256(1))); - } -} diff --git a/src/test/WETH.t.sol b/src/test/WETH.t.sol index 36bfa92b..f2d8b9e1 100644 --- a/src/test/WETH.t.sol +++ b/src/test/WETH.t.sol @@ -64,7 +64,7 @@ contract WETHTest is DSTestPlus { } function testDeposit(uint256 amount) public { - if (amount > address(this).balance) return; + amount = bound(amount, 0, address(this).balance); assertEq(weth.balanceOf(address(this)), 0); assertEq(weth.totalSupply(), 0); @@ -76,7 +76,7 @@ contract WETHTest is DSTestPlus { } function testFallbackDeposit(uint256 amount) public { - if (amount > address(this).balance) return; + amount = bound(amount, 0, address(this).balance); assertEq(weth.balanceOf(address(this)), 0); assertEq(weth.totalSupply(), 0); @@ -88,8 +88,8 @@ contract WETHTest is DSTestPlus { } function testWithdraw(uint256 depositAmount, uint256 withdrawAmount) public { - if (depositAmount > address(this).balance) return; - if (withdrawAmount > depositAmount) return; + depositAmount = bound(depositAmount, 0, address(this).balance); + withdrawAmount = bound(withdrawAmount, 0, depositAmount); weth.deposit{value: depositAmount}(); diff --git a/src/test/utils/DSInvariantTest.sol b/src/test/utils/DSInvariantTest.sol index e42aa5b5..820775c5 100644 --- a/src/test/utils/DSInvariantTest.sol +++ b/src/test/utils/DSInvariantTest.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: AGPL-3.0-only -pragma solidity >=0.7.0; +pragma solidity >=0.8.0; contract DSInvariantTest { address[] private targets; diff --git a/src/test/utils/DSTestPlus.sol b/src/test/utils/DSTestPlus.sol index 82dd53ee..b332b7ab 100644 --- a/src/test/utils/DSTestPlus.sol +++ b/src/test/utils/DSTestPlus.sol @@ -1,10 +1,12 @@ // SPDX-License-Identifier: AGPL-3.0-only -pragma solidity >=0.7.0; +pragma solidity >=0.8.0; import {DSTest} from "ds-test/test.sol"; import {Hevm} from "./Hevm.sol"; +/// @notice Extended testing framework for DappTools projects. +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/test/utils/DSTestPlus.sol) contract DSTestPlus is DSTest { Hevm internal constant hevm = Hevm(HEVM_ADDRESS); @@ -51,6 +53,47 @@ contract DSTestPlus is DSTest { assertEq(uint256(a), uint256(b)); } + function assertBoolEq(bool a, bool b) internal virtual { + b ? assertTrue(a) : assertFalse(a); + } + + function assertApproxEq( + uint256 a, + uint256 b, + uint256 maxDelta + ) internal virtual { + uint256 delta = a > b ? a - b : b - a; + + if (delta > maxDelta) { + emit log("Error: a ~= b not satisfied [uint]"); + emit log_named_uint(" Expected", a); + emit log_named_uint(" Actual", b); + emit log_named_uint(" Max Delta", maxDelta); + emit log_named_uint(" Delta", delta); + fail(); + } + } + + function assertRelApproxEq( + uint256 a, + uint256 b, + uint256 maxPercentDelta + ) internal virtual { + uint256 delta = a > b ? a - b : b - a; + uint256 abs = a > b ? a : b; + + uint256 percentDelta = (delta * 1e18) / abs; + + if (percentDelta > maxPercentDelta) { + emit log("Error: a ~= b not satisfied [uint]"); + emit log_named_uint(" Expected", a); + emit log_named_uint(" Actual", b); + emit log_named_uint(" Max % Delta", maxPercentDelta); + emit log_named_uint(" % Delta", percentDelta); + fail(); + } + } + function assertBytesEq(bytes memory a, bytes memory b) internal virtual { if (keccak256(a) != keccak256(b)) { emit log("Error: a == b not satisfied [bytes]"); @@ -59,4 +102,45 @@ contract DSTestPlus is DSTest { fail(); } } + + function assertUintArrayEq(uint256[] memory a, uint256[] memory b) internal virtual { + require(a.length == b.length, "LENGTH_MISMATCH"); + + for (uint256 i = 0; i < a.length; i++) { + assertEq(a[i], b[i]); + } + } + + function bound( + uint256 x, + uint256 min, + uint256 max + ) internal pure returns (uint256 result) { + require(max >= min, "MAX_LESS_THAN_MIN"); + + uint256 size = max - min; + + if (max != type(uint256).max) size++; // Make the max inclusive. + if (size == 0) return min; // Using max would be equivalent as well. + // Ensure max is inclusive in cases where x != 0 and max is at uint max. + if (max == type(uint256).max && x != 0) x--; // Accounted for later. + + if (x < min) x += size * (((min - x) / size) + 1); + result = min + ((x - min) % size); + + // Account for decrementing x to make max inclusive. + if (max == type(uint256).max && x != 0) result++; + } + + function min3( + uint256 a, + uint256 b, + uint256 c + ) internal pure returns (uint256) { + return a > b ? (b > c ? c : b) : (a > c ? c : a); + } + + function min2(uint256 a, uint256 b) internal pure returns (uint256) { + return a > b ? b : a; + } } diff --git a/src/test/utils/Hevm.sol b/src/test/utils/Hevm.sol index 7e243947..ba18c2c0 100644 --- a/src/test/utils/Hevm.sol +++ b/src/test/utils/Hevm.sol @@ -1,6 +1,5 @@ // SPDX-License-Identifier: AGPL-3.0-only -pragma solidity >=0.7.0; -pragma abicoder v2; +pragma solidity >=0.8.0; interface Hevm { function warp(uint256) external; diff --git a/src/test/utils/mocks/MockAuthChild.sol b/src/test/utils/mocks/MockAuthChild.sol index 8332fe78..d2c32760 100644 --- a/src/test/utils/mocks/MockAuthChild.sol +++ b/src/test/utils/mocks/MockAuthChild.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: AGPL-3.0-only -pragma solidity >=0.7.0; +pragma solidity >=0.8.0; import {Auth, Authority} from "../../../auth/Auth.sol"; diff --git a/src/test/utils/mocks/MockAuthority.sol b/src/test/utils/mocks/MockAuthority.sol new file mode 100644 index 00000000..acb36892 --- /dev/null +++ b/src/test/utils/mocks/MockAuthority.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.0; + +import {Authority} from "../../../auth/Auth.sol"; + +contract MockAuthority is Authority { + bool immutable allowCalls; + + constructor(bool _allowCalls) { + allowCalls = _allowCalls; + } + + function canCall( + address, + address, + bytes4 + ) public view override returns (bool) { + return allowCalls; + } +} diff --git a/src/test/utils/mocks/MockERC1155.sol b/src/test/utils/mocks/MockERC1155.sol new file mode 100644 index 00000000..ede086db --- /dev/null +++ b/src/test/utils/mocks/MockERC1155.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.0; + +import {ERC1155} from "../../../tokens/ERC1155.sol"; + +contract MockERC1155 is ERC1155 { + function uri(uint256) public pure virtual override returns (string memory) {} + + function mint( + address to, + uint256 id, + uint256 amount, + bytes memory data + ) public virtual { + _mint(to, id, amount, data); + } + + function batchMint( + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) public virtual { + _batchMint(to, ids, amounts, data); + } + + function burn( + address from, + uint256 id, + uint256 amount + ) public virtual { + _burn(from, id, amount); + } + + function batchBurn( + address from, + uint256[] memory ids, + uint256[] memory amounts + ) public virtual { + _batchBurn(from, ids, amounts); + } +} diff --git a/src/test/utils/mocks/MockERC721.sol b/src/test/utils/mocks/MockERC721.sol new file mode 100644 index 00000000..51227c0e --- /dev/null +++ b/src/test/utils/mocks/MockERC721.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.0; + +import {ERC721} from "../../../tokens/ERC721.sol"; + +contract MockERC721 is ERC721 { + constructor(string memory _name, string memory _symbol) ERC721(_name, _symbol) {} + + function tokenURI(uint256) public pure virtual override returns (string memory) {} + + function mint(address to, uint256 tokenId) public virtual { + _mint(to, tokenId); + } + + function burn(uint256 tokenId) public virtual { + _burn(tokenId); + } + + function safeMint(address to, uint256 tokenId) public virtual { + _safeMint(to, tokenId); + } + + function safeMint( + address to, + uint256 tokenId, + bytes memory data + ) public virtual { + _safeMint(to, tokenId, data); + } +} diff --git a/src/test/utils/mocks/MockTrustChild.sol b/src/test/utils/mocks/MockTrustChild.sol deleted file mode 100644 index ad2c2922..00000000 --- a/src/test/utils/mocks/MockTrustChild.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity >=0.7.0; - -import {Trust} from "../../../auth/Trust.sol"; - -contract MockTrustChild is Trust(msg.sender) { - bool public flag; - - function updateFlag() public virtual requiresTrust { - flag = true; - } -} diff --git a/src/test/utils/users/ERC1155User.sol b/src/test/utils/users/ERC1155User.sol new file mode 100644 index 00000000..f59cf107 --- /dev/null +++ b/src/test/utils/users/ERC1155User.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.0; + +import {ERC1155, ERC1155TokenReceiver} from "../../../tokens/ERC1155.sol"; + +contract ERC1155User is ERC1155TokenReceiver { + ERC1155 token; + + constructor(ERC1155 _token) { + token = _token; + } + + function onERC1155Received( + address, + address, + uint256, + uint256, + bytes calldata + ) external virtual override returns (bytes4) { + return ERC1155TokenReceiver.onERC1155Received.selector; + } + + function onERC1155BatchReceived( + address, + address, + uint256[] calldata, + uint256[] calldata, + bytes calldata + ) external virtual override returns (bytes4) { + return ERC1155TokenReceiver.onERC1155BatchReceived.selector; + } + + function setApprovalForAll(address operator, bool approved) public virtual { + token.setApprovalForAll(operator, approved); + } + + function safeTransferFrom( + address from, + address to, + uint256 id, + uint256 amount, + bytes memory data + ) public virtual { + token.safeTransferFrom(from, to, id, amount, data); + } + + function safeBatchTransferFrom( + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) public virtual { + token.safeBatchTransferFrom(from, to, ids, amounts, data); + } +} diff --git a/src/test/utils/users/ERC721User.sol b/src/test/utils/users/ERC721User.sol new file mode 100644 index 00000000..dea9c938 --- /dev/null +++ b/src/test/utils/users/ERC721User.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.0; + +import {ERC721, ERC721TokenReceiver} from "../../../tokens/ERC721.sol"; + +contract ERC721User is ERC721TokenReceiver { + ERC721 token; + + constructor(ERC721 _token) { + token = _token; + } + + function onERC721Received( + address, + address, + uint256, + bytes calldata + ) public virtual override returns (bytes4) { + return ERC721TokenReceiver.onERC721Received.selector; + } + + function approve(address spender, uint256 tokenId) public virtual { + token.approve(spender, tokenId); + } + + function setApprovalForAll(address operator, bool approved) public virtual { + token.setApprovalForAll(operator, approved); + } + + function transferFrom( + address from, + address to, + uint256 tokenId + ) public virtual { + token.transferFrom(from, to, tokenId); + } + + function safeTransferFrom( + address from, + address to, + uint256 tokenId + ) public virtual { + token.safeTransferFrom(from, to, tokenId); + } + + function safeTransferFrom( + address from, + address to, + uint256 tokenId, + bytes memory data + ) public { + token.safeTransferFrom(from, to, tokenId, data); + } +} diff --git a/src/test/utils/users/GenericUser.sol b/src/test/utils/users/GenericUser.sol index d593f315..6680bf21 100644 --- a/src/test/utils/users/GenericUser.sol +++ b/src/test/utils/users/GenericUser.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity >=0.7.0; +pragma solidity >=0.8.0; contract GenericUser { function tryCall(address target, bytes memory data) public virtual returns (bool success, bytes memory returnData) { @@ -17,7 +17,7 @@ contract GenericUser { revert(add(32, returnData), returnDataSize) } } else { - revert("REVERTED_WITHOUT_MESSAGE"); + revert("REVERTED_WITHOUT_A_MESSAGE"); } } } diff --git a/src/tokens/ERC1155.sol b/src/tokens/ERC1155.sol new file mode 100644 index 00000000..0b018b51 --- /dev/null +++ b/src/tokens/ERC1155.sol @@ -0,0 +1,253 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.0; + +/// @notice Minimalist and gas efficient standard ERC1155 implementation. +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/tokens/ERC1155.sol) +abstract contract ERC1155 { + /*/////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + event TransferSingle( + address indexed operator, + address indexed from, + address indexed to, + uint256 id, + uint256 amount + ); + + event TransferBatch( + address indexed operator, + address indexed from, + address indexed to, + uint256[] ids, + uint256[] amounts + ); + + event ApprovalForAll(address indexed owner, address indexed operator, bool approved); + + event URI(string value, uint256 indexed id); + + /*/////////////////////////////////////////////////////////////// + ERC1155 STORAGE + //////////////////////////////////////////////////////////////*/ + + mapping(address => mapping(uint256 => uint256)) public balanceOf; + + mapping(address => mapping(address => bool)) public isApprovedForAll; + + /*/////////////////////////////////////////////////////////////// + METADATA LOGIC + //////////////////////////////////////////////////////////////*/ + + function uri(uint256 id) public view virtual returns (string memory); + + /*/////////////////////////////////////////////////////////////// + ERC1155 ACTIONS + //////////////////////////////////////////////////////////////*/ + + function setApprovalForAll(address operator, bool approved) public virtual { + isApprovedForAll[msg.sender][operator] = approved; + + emit ApprovalForAll(msg.sender, operator, approved); + } + + function safeTransferFrom( + address from, + address to, + uint256 id, + uint256 amount, + bytes memory data + ) public virtual { + require(msg.sender == from || isApprovedForAll[from][msg.sender], "NOT_AUTHORIZED"); + + balanceOf[from][id] -= amount; + balanceOf[to][id] += amount; + + emit TransferSingle(msg.sender, from, to, id, amount); + + require( + to.code.length == 0 + ? to != address(0) + : ERC1155TokenReceiver(to).onERC1155Received(msg.sender, from, id, amount, data) == + ERC1155TokenReceiver.onERC1155Received.selector, + "UNSAFE_RECIPIENT" + ); + } + + function safeBatchTransferFrom( + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) public virtual { + uint256 idsLength = ids.length; // Saves MLOADs. + + require(idsLength == amounts.length, "LENGTH_MISMATCH"); + + require(msg.sender == from || isApprovedForAll[from][msg.sender], "NOT_AUTHORIZED"); + + for (uint256 i = 0; i < idsLength; ) { + uint256 id = ids[i]; + uint256 amount = amounts[i]; + + balanceOf[from][id] -= amount; + balanceOf[to][id] += amount; + + // An array can't have a total length + // larger than the max uint256 value. + unchecked { + i++; + } + } + + emit TransferBatch(msg.sender, from, to, ids, amounts); + + require( + to.code.length == 0 + ? to != address(0) + : ERC1155TokenReceiver(to).onERC1155BatchReceived(msg.sender, from, ids, amounts, data) == + ERC1155TokenReceiver.onERC1155BatchReceived.selector, + "UNSAFE_RECIPIENT" + ); + } + + function balanceOfBatch(address[] memory owners, uint256[] memory ids) + public + view + virtual + returns (uint256[] memory balances) + { + uint256 ownersLength = owners.length; // Saves MLOADs. + + require(ownersLength == ids.length, "LENGTH_MISMATCH"); + + balances = new uint256[](owners.length); + + // Unchecked because the only math done is incrementing + // the array index counter which cannot possibly overflow. + unchecked { + for (uint256 i = 0; i < ownersLength; i++) { + balances[i] = balanceOf[owners[i]][ids[i]]; + } + } + } + + /*/////////////////////////////////////////////////////////////// + ERC165 LOGIC + //////////////////////////////////////////////////////////////*/ + + function supportsInterface(bytes4 interfaceId) public pure virtual returns (bool) { + return + interfaceId == 0x01ffc9a7 || // ERC165 Interface ID for ERC165 + interfaceId == 0xd9b67a26 || // ERC165 Interface ID for ERC1155 + interfaceId == 0x0e89341c; // ERC165 Interface ID for ERC1155MetadataURI + } + + /*/////////////////////////////////////////////////////////////// + INTERNAL MINT/BURN LOGIC + //////////////////////////////////////////////////////////////*/ + + function _mint( + address to, + uint256 id, + uint256 amount, + bytes memory data + ) internal { + balanceOf[to][id] += amount; + + emit TransferSingle(msg.sender, address(0), to, id, amount); + + require( + to.code.length == 0 + ? to != address(0) + : ERC1155TokenReceiver(to).onERC1155Received(msg.sender, address(0), id, amount, data) == + ERC1155TokenReceiver.onERC1155Received.selector, + "UNSAFE_RECIPIENT" + ); + } + + function _batchMint( + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) internal { + uint256 idsLength = ids.length; // Saves MLOADs. + + require(idsLength == amounts.length, "LENGTH_MISMATCH"); + + for (uint256 i = 0; i < idsLength; ) { + balanceOf[to][ids[i]] += amounts[i]; + + // An array can't have a total length + // larger than the max uint256 value. + unchecked { + i++; + } + } + + emit TransferBatch(msg.sender, address(0), to, ids, amounts); + + require( + to.code.length == 0 + ? to != address(0) + : ERC1155TokenReceiver(to).onERC1155BatchReceived(msg.sender, address(0), ids, amounts, data) == + ERC1155TokenReceiver.onERC1155BatchReceived.selector, + "UNSAFE_RECIPIENT" + ); + } + + function _batchBurn( + address from, + uint256[] memory ids, + uint256[] memory amounts + ) internal { + uint256 idsLength = ids.length; // Saves MLOADs. + + require(idsLength == amounts.length, "LENGTH_MISMATCH"); + + for (uint256 i = 0; i < idsLength; ) { + balanceOf[from][ids[i]] -= amounts[i]; + + // An array can't have a total length + // larger than the max uint256 value. + unchecked { + i++; + } + } + + emit TransferBatch(msg.sender, from, address(0), ids, amounts); + } + + function _burn( + address from, + uint256 id, + uint256 amount + ) internal { + balanceOf[from][id] -= amount; + + emit TransferSingle(msg.sender, from, address(0), id, amount); + } +} + +/// @notice A generic interface for a contract which properly accepts ERC1155 tokens. +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/tokens/ERC1155.sol) +interface ERC1155TokenReceiver { + function onERC1155Received( + address operator, + address from, + uint256 id, + uint256 amount, + bytes calldata data + ) external returns (bytes4); + + function onERC1155BatchReceived( + address operator, + address from, + uint256[] calldata ids, + uint256[] calldata amounts, + bytes calldata data + ) external returns (bytes4); +} diff --git a/src/tokens/ERC20.sol b/src/tokens/ERC20.sol index abe088e5..9d6fe64f 100644 --- a/src/tokens/ERC20.sol +++ b/src/tokens/ERC20.sol @@ -2,7 +2,9 @@ pragma solidity >=0.8.0; /// @notice Modern and gas efficient ERC20 + EIP-2612 implementation. +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/tokens/ERC20.sol) /// @author Modified from Uniswap (https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/UniswapV2ERC20.sol) +/// @dev Do not manually set balances without updating totalSupply, as the sum of all user balances must not exceed it. abstract contract ERC20 { /*/////////////////////////////////////////////////////////////// EVENTS @@ -33,7 +35,7 @@ abstract contract ERC20 { mapping(address => mapping(address => uint256)) public allowance; /*/////////////////////////////////////////////////////////////// - EIP-2612 STORAGE + EIP-2612 STORAGE //////////////////////////////////////////////////////////////*/ bytes32 public constant PERMIT_TYPEHASH = @@ -93,9 +95,9 @@ abstract contract ERC20 { address to, uint256 amount ) public virtual returns (bool) { - if (allowance[from][msg.sender] != type(uint256).max) { - allowance[from][msg.sender] -= amount; - } + uint256 allowed = allowance[from][msg.sender]; // Saves gas for limited approvals. + + if (allowed != type(uint256).max) allowance[from][msg.sender] = allowed - amount; balanceOf[from] -= amount; @@ -137,7 +139,8 @@ abstract contract ERC20 { ); address recoveredAddress = ecrecover(digest, v, r, s); - require(recoveredAddress != address(0) && recoveredAddress == owner, "INVALID_PERMIT_SIGNATURE"); + + require(recoveredAddress != address(0) && recoveredAddress == owner, "INVALID_SIGNER"); allowance[recoveredAddress][spender] = value; } @@ -155,7 +158,7 @@ abstract contract ERC20 { abi.encode( keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), keccak256(bytes(name)), - keccak256(bytes("1")), + keccak256("1"), block.chainid, address(this) ) diff --git a/src/tokens/ERC721.sol b/src/tokens/ERC721.sol new file mode 100644 index 00000000..33946f91 --- /dev/null +++ b/src/tokens/ERC721.sol @@ -0,0 +1,220 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.0; + +/// @notice Modern, minimalist, and gas efficient ERC-721 implementation. +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/tokens/ERC721.sol) +/// @dev Note that balanceOf does not revert if passed the zero address, in defiance of the ERC. +abstract contract ERC721 { + /*/////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + event Transfer(address indexed from, address indexed to, uint256 indexed id); + + event Approval(address indexed owner, address indexed spender, uint256 indexed id); + + event ApprovalForAll(address indexed owner, address indexed operator, bool approved); + + /*/////////////////////////////////////////////////////////////// + METADATA STORAGE/LOGIC + //////////////////////////////////////////////////////////////*/ + + string public name; + + string public symbol; + + function tokenURI(uint256 id) public view virtual returns (string memory); + + /*/////////////////////////////////////////////////////////////// + ERC721 STORAGE + //////////////////////////////////////////////////////////////*/ + + uint256 public totalSupply; + + mapping(address => uint256) public balanceOf; + + mapping(uint256 => address) public ownerOf; + + mapping(uint256 => address) public getApproved; + + mapping(address => mapping(address => bool)) public isApprovedForAll; + + /*/////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + constructor(string memory _name, string memory _symbol) { + name = _name; + symbol = _symbol; + } + + /*/////////////////////////////////////////////////////////////// + ERC721 LOGIC + //////////////////////////////////////////////////////////////*/ + + function approve(address spender, uint256 id) public virtual { + address owner = ownerOf[id]; + + require(msg.sender == owner || isApprovedForAll[owner][msg.sender], "NOT_AUTHORIZED"); + + getApproved[id] = spender; + + emit Approval(owner, spender, id); + } + + function setApprovalForAll(address operator, bool approved) public virtual { + isApprovedForAll[msg.sender][operator] = approved; + + emit ApprovalForAll(msg.sender, operator, approved); + } + + function transferFrom( + address from, + address to, + uint256 id + ) public virtual { + require(from == ownerOf[id], "WRONG_FROM"); + + require(to != address(0), "INVALID_RECIPIENT"); + + require( + msg.sender == from || msg.sender == getApproved[id] || isApprovedForAll[from][msg.sender], + "NOT_AUTHORIZED" + ); + + // Underflow of the sender's balance is impossible because we check for + // ownership above and the recipient's balance can't realistically overflow. + unchecked { + balanceOf[from]--; + + balanceOf[to]++; + } + + delete getApproved[id]; + + ownerOf[id] = to; + + emit Transfer(from, to, id); + } + + function safeTransferFrom( + address from, + address to, + uint256 id + ) public virtual { + transferFrom(from, to, id); + + require( + to.code.length == 0 || + ERC721TokenReceiver(to).onERC721Received(msg.sender, from, id, "") == + ERC721TokenReceiver.onERC721Received.selector, + "UNSAFE_RECIPIENT" + ); + } + + function safeTransferFrom( + address from, + address to, + uint256 id, + bytes memory data + ) public virtual { + transferFrom(from, to, id); + + require( + to.code.length == 0 || + ERC721TokenReceiver(to).onERC721Received(msg.sender, from, id, data) == + ERC721TokenReceiver.onERC721Received.selector, + "UNSAFE_RECIPIENT" + ); + } + + /*/////////////////////////////////////////////////////////////// + ERC165 LOGIC + //////////////////////////////////////////////////////////////*/ + + function supportsInterface(bytes4 interfaceId) public pure virtual returns (bool) { + return + interfaceId == 0x01ffc9a7 || // ERC165 Interface ID for ERC165 + interfaceId == 0x80ac58cd || // ERC165 Interface ID for ERC721 + interfaceId == 0x5b5e139f; // ERC165 Interface ID for ERC721Metadata + } + + /*/////////////////////////////////////////////////////////////// + INTERNAL MINT/BURN LOGIC + //////////////////////////////////////////////////////////////*/ + + function _mint(address to, uint256 id) internal virtual { + require(to != address(0), "INVALID_RECIPIENT"); + + require(ownerOf[id] == address(0), "ALREADY_MINTED"); + + // Counter overflow is incredibly unrealistic. + unchecked { + totalSupply++; + + balanceOf[to]++; + } + + ownerOf[id] = to; + + emit Transfer(address(0), to, id); + } + + function _burn(uint256 id) internal virtual { + address owner = ownerOf[id]; + + require(ownerOf[id] != address(0), "NOT_MINTED"); + + // Ownership check above ensures no underflow. + unchecked { + totalSupply--; + + balanceOf[owner]--; + } + + delete ownerOf[id]; + + emit Transfer(owner, address(0), id); + } + + /*/////////////////////////////////////////////////////////////// + INTERNAL SAFE MINT LOGIC + //////////////////////////////////////////////////////////////*/ + + function _safeMint(address to, uint256 id) internal virtual { + _mint(to, id); + + require( + to.code.length == 0 || + ERC721TokenReceiver(to).onERC721Received(msg.sender, address(0), id, "") == + ERC721TokenReceiver.onERC721Received.selector, + "UNSAFE_RECIPIENT" + ); + } + + function _safeMint( + address to, + uint256 id, + bytes memory data + ) internal virtual { + _mint(to, id); + + require( + to.code.length == 0 || + ERC721TokenReceiver(to).onERC721Received(msg.sender, address(0), id, data) == + ERC721TokenReceiver.onERC721Received.selector, + "UNSAFE_RECIPIENT" + ); + } +} + +/// @notice A generic interface for a contract which properly accepts ERC721 tokens. +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/tokens/ERC721.sol) +interface ERC721TokenReceiver { + function onERC721Received( + address operator, + address from, + uint256 id, + bytes calldata data + ) external returns (bytes4); +} diff --git a/src/tokens/WETH.sol b/src/tokens/WETH.sol index 39513c62..5c470e37 100644 --- a/src/tokens/WETH.sol +++ b/src/tokens/WETH.sol @@ -6,6 +6,7 @@ import {ERC20} from "./ERC20.sol"; import {SafeTransferLib} from "../utils/SafeTransferLib.sol"; /// @notice Minimalist and modern Wrapped Ether implementation. +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/tokens/WETH.sol) /// @author Inspired by WETH9 (https://github.com/dapphub/ds-weth/blob/master/src/weth9.sol) contract WETH is ERC20("Wrapped Ether", "WETH", 18) { using SafeTransferLib for address; @@ -14,21 +15,21 @@ contract WETH is ERC20("Wrapped Ether", "WETH", 18) { event Withdrawal(address indexed to, uint256 amount); - function deposit() public payable { + function deposit() public payable virtual { _mint(msg.sender, msg.value); emit Deposit(msg.sender, msg.value); } - function withdraw(uint256 amount) external { + function withdraw(uint256 amount) public virtual { _burn(msg.sender, amount); - msg.sender.safeTransferETH(amount); - emit Withdrawal(msg.sender, amount); + + msg.sender.safeTransferETH(amount); } - receive() external payable { + receive() external payable virtual { deposit(); } } diff --git a/src/utils/Bytes32AddressLib.sol b/src/utils/Bytes32AddressLib.sol index 13d9857a..bc857be1 100644 --- a/src/utils/Bytes32AddressLib.sol +++ b/src/utils/Bytes32AddressLib.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: AGPL-3.0-only -pragma solidity >=0.7.0; +pragma solidity >=0.8.0; /// @notice Library for converting between addresses and bytes32 values. -/// @author Original work by Transmissions11 (https://github.com/transmissions11) +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/utils/Bytes32AddressLib.sol) library Bytes32AddressLib { function fromLast20Bytes(bytes32 bytesValue) internal pure returns (address) { return address(uint160(uint256(bytesValue))); diff --git a/src/utils/CREATE3.sol b/src/utils/CREATE3.sol index 0c43d594..04e09155 100644 --- a/src/utils/CREATE3.sol +++ b/src/utils/CREATE3.sol @@ -1,29 +1,56 @@ // SPDX-License-Identifier: AGPL-3.0-only -pragma solidity >=0.7.0; +pragma solidity >=0.8.0; import {Bytes32AddressLib} from "./Bytes32AddressLib.sol"; /// @notice Deploy to deterministic addresses without an initcode factor. -/// @author Modified from 0xSequence (https://github.com/0xsequence/create3/blob/master/contracts/Create3.sol) +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/utils/CREATE3.sol) +/// @author Modified from 0xSequence (https://github.com/0xSequence/create3/blob/master/contracts/Create3.sol) library CREATE3 { using Bytes32AddressLib for bytes32; + //--------------------------------------------------------------------------------// + // Opcode | Opcode + Arguments | Description | Stack View // + //--------------------------------------------------------------------------------// + // 0x36 | 0x36 | CALLDATASIZE | size // + // 0x3d | 0x3d | RETURNDATASIZE | 0 size // + // 0x3d | 0x3d | RETURNDATASIZE | 0 0 size // + // 0x37 | 0x37 | CALLDATACOPY | // + // 0x36 | 0x36 | CALLDATASIZE | size // + // 0x3d | 0x3d | RETURNDATASIZE | 0 size // + // 0x34 | 0x34 | CALLVALUE | value 0 size // + // 0xf0 | 0xf0 | CREATE | newContract // + //--------------------------------------------------------------------------------// + // Opcode | Opcode + Arguments | Description | Stack View // + //--------------------------------------------------------------------------------// + // 0x67 | 0x67XXXXXXXXXXXXXXXX | PUSH8 bytecode | bytecode // + // 0x3d | 0x3d | RETURNDATASIZE | 0 bytecode // + // 0x52 | 0x52 | MSTORE | // + // 0x60 | 0x6008 | PUSH1 08 | 8 // + // 0x60 | 0x6018 | PUSH1 18 | 24 8 // + // 0xf3 | 0xf3 | RETURN | // + //--------------------------------------------------------------------------------// bytes internal constant PROXY_BYTECODE = hex"67_36_3d_3d_37_36_3d_34_f0_3d_52_60_08_60_18_f3"; bytes32 internal constant PROXY_BYTECODE_HASH = keccak256(PROXY_BYTECODE); - function deploy(bytes32 salt, bytes memory creationCode) internal returns (address deployed) { + function deploy( + bytes32 salt, + bytes memory creationCode, + uint256 value + ) internal returns (address deployed) { bytes memory proxyChildBytecode = PROXY_BYTECODE; - deployed = getDeployed(salt); - address proxy; assembly { + // Deploy a new contract with our pre-made bytecode via CREATE2. + // We start 32 bytes into the code to avoid copying the byte length. proxy := create2(0, add(proxyChildBytecode, 32), mload(proxyChildBytecode), salt) } require(proxy != address(0), "DEPLOYMENT_FAILED"); - (bool success, ) = proxy.call(creationCode); + deployed = getDeployed(salt); + (bool success, ) = proxy.call{value: value}(creationCode); require(success && deployed.code.length != 0, "INITIALIZATION_FAILED"); } @@ -41,6 +68,15 @@ library CREATE3 { ) ).fromLast20Bytes(); - return keccak256(abi.encodePacked(hex"d6_94", proxy, hex"01")).fromLast20Bytes(); + return + keccak256( + abi.encodePacked( + // 0xd6 = 0xc0 (short RLP prefix) + 0x16 (length of: 0x94 ++ proxy ++ 0x01) + // 0x94 = 0x80 + 0x14 (0x14 = the length of an address, 20 bytes, in hex) + hex"d6_94", + proxy, + hex"01" // Nonce of the proxy contract (1) + ) + ).fromLast20Bytes(); } } diff --git a/src/utils/FixedPointMathLib.sol b/src/utils/FixedPointMathLib.sol index 50536895..d01a2be1 100644 --- a/src/utils/FixedPointMathLib.sol +++ b/src/utils/FixedPointMathLib.sol @@ -2,8 +2,7 @@ pragma solidity >=0.8.0; /// @notice Arithmetic library with operations for fixed-point numbers. -/// @author Modified from Dappsys V2 (https://github.com/dapp-org/dappsys-v2/blob/main/src/math.sol) -/// and ABDK (https://github.com/abdk-consulting/abdk-libraries-solidity/blob/master/ABDKMath64x64.sol) +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/utils/FixedPointMathLib.sol) library FixedPointMathLib { /*/////////////////////////////////////////////////////////////// COMMON BASE UNITS @@ -46,12 +45,8 @@ library FixedPointMathLib { // Store x * baseUnit in z for now. z := mul(x, baseUnit) - if or( - // Revert if y is zero to ensure we don't divide by zero below. - iszero(y), - // Equivalent to require(x == 0 || (x * baseUnit) / x == baseUnit) - iszero(or(iszero(x), eq(div(z, x), baseUnit))) - ) { + // Equivalent to require(y != 0 && (x == 0 || (x * baseUnit) / x == baseUnit)) + if iszero(and(iszero(iszero(y)), or(iszero(x), eq(div(z, x), baseUnit)))) { revert(0, 0) } @@ -70,44 +65,77 @@ library FixedPointMathLib { case 0 { switch n case 0 { + // 0 ** 0 = 1 z := baseUnit } default { + // 0 ** n = 0 z := 0 } } default { switch mod(n, 2) case 0 { + // If n is even, store baseUnit in z for now. z := baseUnit } default { + // If n is odd, store x in z for now. z := x } - let half := div(baseUnit, 2) + + // Shifting right by 1 is like dividing by 2. + let half := shr(1, baseUnit) + for { - n := div(n, 2) + // Shift n right by 1 before looping to halve it. + n := shr(1, n) } n { - n := div(n, 2) + // Shift n right by 1 each iteration to halve it. + n := shr(1, n) } { - let xx := mul(x, x) - if iszero(eq(div(xx, x), x)) { + // Revert immediately if x ** 2 would overflow. + // Equivalent to iszero(eq(div(xx, x), x)) here. + if shr(128, x) { revert(0, 0) } + + // Store x squared. + let xx := mul(x, x) + + // Round to the nearest number. let xxRound := add(xx, half) + + // Revert if xx + half overflowed. if lt(xxRound, xx) { revert(0, 0) } + + // Set x to scaled xxRound. x := div(xxRound, baseUnit) + + // If n is even: if mod(n, 2) { + // Compute z * x. let zx := mul(z, x) - if and(iszero(iszero(x)), iszero(eq(div(zx, x), z))) { - revert(0, 0) + + // If z * x overflowed: + if iszero(eq(div(zx, x), z)) { + // Revert if x is non-zero. + if iszero(iszero(x)) { + revert(0, 0) + } } + + // Round to the nearest number. let zxRound := add(zx, half) + + // Revert if zx + half overflowed. if lt(zxRound, zx) { revert(0, 0) } + + // Return properly scaled zxRound. z := div(zxRound, baseUnit) } } @@ -119,65 +147,60 @@ library FixedPointMathLib { GENERAL NUMBER UTILITIES //////////////////////////////////////////////////////////////*/ - function sqrt(uint256 x) internal pure returns (uint256 result) { - if (x == 0) return 0; - - result = 1; - - uint256 xAux = x; - - if (xAux >= 0x100000000000000000000000000000000) { - xAux >>= 128; - result <<= 64; - } - - if (xAux >= 0x10000000000000000) { - xAux >>= 64; - result <<= 32; - } - - if (xAux >= 0x100000000) { - xAux >>= 32; - result <<= 16; - } - - if (xAux >= 0x10000) { - xAux >>= 16; - result <<= 8; - } - - if (xAux >= 0x100) { - xAux >>= 8; - result <<= 4; - } - - if (xAux >= 0x10) { - xAux >>= 4; - result <<= 2; - } - - if (xAux >= 0x8) result <<= 1; + function sqrt(uint256 x) internal pure returns (uint256 z) { + assembly { + // Start off with z at 1. + z := 1 - unchecked { - result = (result + x / result) >> 1; - result = (result + x / result) >> 1; - result = (result + x / result) >> 1; - result = (result + x / result) >> 1; - result = (result + x / result) >> 1; - result = (result + x / result) >> 1; - result = (result + x / result) >> 1; + // Used below to help find a nearby power of 2. + let y := x - uint256 roundedDownResult = x / result; + // Find the lowest power of 2 that is at least sqrt(x). + if iszero(lt(y, 0x100000000000000000000000000000000)) { + y := shr(128, y) // Like dividing by 2 ** 128. + z := shl(64, z) + } + if iszero(lt(y, 0x10000000000000000)) { + y := shr(64, y) // Like dividing by 2 ** 64. + z := shl(32, z) + } + if iszero(lt(y, 0x100000000)) { + y := shr(32, y) // Like dividing by 2 ** 32. + z := shl(16, z) + } + if iszero(lt(y, 0x10000)) { + y := shr(16, y) // Like dividing by 2 ** 16. + z := shl(8, z) + } + if iszero(lt(y, 0x100)) { + y := shr(8, y) // Like dividing by 2 ** 8. + z := shl(4, z) + } + if iszero(lt(y, 0x10)) { + y := shr(4, y) // Like dividing by 2 ** 4. + z := shl(2, z) + } + if iszero(lt(y, 0x8)) { + // Equivalent to 2 ** z. + z := shl(1, z) + } - if (result > roundedDownResult) result = roundedDownResult; + // Shifting right by 1 is like dividing by 2. + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + + // Compute a rounded down version of z. + let zRoundDown := div(x, z) + + // If zRoundDown is smaller, use it. + if lt(zRoundDown, z) { + z := zRoundDown + } } } - - function min(uint256 x, uint256 y) internal pure returns (uint256 z) { - return x < y ? x : y; - } - - function max(uint256 x, uint256 y) internal pure returns (uint256 z) { - return x > y ? x : y; - } } diff --git a/src/utils/ReentrancyGuard.sol b/src/utils/ReentrancyGuard.sol index a462fe83..9caae5b8 100644 --- a/src/utils/ReentrancyGuard.sol +++ b/src/utils/ReentrancyGuard.sol @@ -1,7 +1,8 @@ // SPDX-License-Identifier: AGPL-3.0-only -pragma solidity >=0.7.0; +pragma solidity >=0.8.0; /// @notice Gas optimized reentrancy protection for smart contracts. +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/utils/ReentrancyGuard.sol) /// @author Modified from OpenZeppelin (https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/security/ReentrancyGuard.sol) abstract contract ReentrancyGuard { uint256 private reentrancyStatus = 1; diff --git a/src/utils/SSTORE2.sol b/src/utils/SSTORE2.sol index 6e388b60..265f4a56 100644 --- a/src/utils/SSTORE2.sol +++ b/src/utils/SSTORE2.sol @@ -1,28 +1,52 @@ // SPDX-License-Identifier: AGPL-3.0-only -pragma solidity >=0.7.0; +pragma solidity >=0.8.0; /// @notice Read and write to persistent storage at a fraction of the cost. -/// @author Modified from 0xSequence (https://github.com/0xsequence/sstore2/blob/master/contracts/SSTORE2.sol) +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/utils/SSTORE2.sol) +/// @author Modified from 0xSequence (https://github.com/0xSequence/sstore2/blob/master/contracts/SSTORE2.sol) library SSTORE2 { - uint256 internal constant DATA_OFFSET = 1; + uint256 internal constant DATA_OFFSET = 1; // We skip the first byte as it's a STOP opcode to ensure the contract can't be called. + + /*/////////////////////////////////////////////////////////////// + WRITE LOGIC + //////////////////////////////////////////////////////////////*/ function write(bytes memory data) internal returns (address pointer) { + // Prefix the bytecode with a STOP opcode to ensure it cannot be called. bytes memory runtimeCode = abi.encodePacked(hex"00", data); bytes memory creationCode = abi.encodePacked( - hex"63", - uint32(runtimeCode.length), - hex"80_60_0E_60_00_39_60_00_F3", - runtimeCode + //---------------------------------------------------------------------------------------------------------------// + // Opcode | Opcode + Arguments | Description | Stack View // + //---------------------------------------------------------------------------------------------------------------// + // 0x60 | 0x600B | PUSH1 11 | codeOffset // + // 0x59 | 0x59 | MSIZE | 0 codeOffset // + // 0x81 | 0x81 | DUP2 | codeOffset 0 codeOffset // + // 0x38 | 0x38 | CODESIZE | codeSize codeOffset 0 codeOffset // + // 0x03 | 0x03 | SUB | (codeSize - codeOffset) 0 codeOffset // + // 0x80 | 0x80 | DUP | (codeSize - codeOffset) (codeSize - codeOffset) 0 codeOffset // + // 0x92 | 0x92 | SWAP3 | codeOffset (codeSize - codeOffset) 0 (codeSize - codeOffset) // + // 0x59 | 0x59 | MSIZE | 0 codeOffset (codeSize - codeOffset) 0 (codeSize - codeOffset) // + // 0x39 | 0x39 | CODECOPY | 0 (codeSize - codeOffset) // + // 0xf3 | 0xf3 | RETURN | // + //---------------------------------------------------------------------------------------------------------------// + hex"60_0B_59_81_38_03_80_92_59_39_F3", // Returns all code in the contract except for the first 11 (0B in hex) bytes. + runtimeCode // The bytecode we want the contract to have after deployment. Capped at 1 byte less than the code size limit. ); assembly { + // Deploy a new contract with the generated creation code. + // We start 32 bytes into the code to avoid copying the byte length. pointer := create(0, add(creationCode, 32), mload(creationCode)) } require(pointer != address(0), "DEPLOYMENT_FAILED"); } + /*/////////////////////////////////////////////////////////////// + READ LOGIC + //////////////////////////////////////////////////////////////*/ + function read(address pointer) internal view returns (bytes memory) { return readBytecode(pointer, DATA_OFFSET, pointer.code.length - DATA_OFFSET); } @@ -46,16 +70,30 @@ library SSTORE2 { return readBytecode(pointer, start, end - start); } + /*/////////////////////////////////////////////////////////////// + INTERNAL HELPER LOGIC + //////////////////////////////////////////////////////////////*/ + function readBytecode( address pointer, uint256 start, uint256 size ) private view returns (bytes memory data) { assembly { + // Get a pointer to some free memory. data := mload(0x40) - mstore(0x40, add(data, and(add(add(size, add(start, 0x20)), 0x1f), not(0x1f)))) + + // Update the free memory pointer to prevent overriding our data. + // We use and(x, not(31)) as a cheaper equivalent to sub(x, mod(x, 32)). + // Adding 31 to size and running the result through the logic above ensures + // the memory pointer remains word-aligned, following the Solidity convention. + mstore(0x40, add(data, and(add(add(size, 32), 31), not(31)))) + + // Store the size of the data in the first 32 byte chunk of free memory. mstore(data, size) - extcodecopy(pointer, add(data, 0x20), start, size) + + // Copy the code into memory right after the 32 bytes we used to store the size. + extcodecopy(pointer, add(data, 32), start, size) } } } diff --git a/src/utils/SafeCastLib.sol b/src/utils/SafeCastLib.sol index cb5adf4e..42011497 100644 --- a/src/utils/SafeCastLib.sol +++ b/src/utils/SafeCastLib.sol @@ -1,7 +1,8 @@ // SPDX-License-Identifier: AGPL-3.0-only -pragma solidity >=0.7.0; +pragma solidity >=0.8.0; /// @notice Safe unsigned integer casting library that reverts on overflow. +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/utils/SafeCastLib.sol) /// @author Modified from OpenZeppelin (https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/math/SafeCast.sol) library SafeCastLib { function safeCastTo248(uint256 x) internal pure returns (uint248 y) { diff --git a/src/utils/SafeTransferLib.sol b/src/utils/SafeTransferLib.sol index e03230f0..2c4526e4 100644 --- a/src/utils/SafeTransferLib.sol +++ b/src/utils/SafeTransferLib.sol @@ -4,6 +4,7 @@ pragma solidity >=0.8.0; import {ERC20} from "../tokens/ERC20.sol"; /// @notice Safe ETH and ERC20 transfer library that gracefully handles missing return values. +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/utils/SafeTransferLib.sol) /// @author Modified from Gnosis (https://github.com/gnosis/gp-v2-contracts/blob/main/src/contracts/libraries/GPv2SafeERC20.sol) /// @dev Use with caution! Some functions in this library knowingly create dirty bits at the destination of the free memory pointer. library SafeTransferLib {