diff --git a/src/enforcers/ERC1155BalanceGteEnforcer.sol b/src/enforcers/ERC1155BalanceGteEnforcer.sol new file mode 100644 index 0000000..572e16f --- /dev/null +++ b/src/enforcers/ERC1155BalanceGteEnforcer.sol @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { IERC1155 } from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; + +import { CaveatEnforcer } from "./CaveatEnforcer.sol"; +import { ModeCode } from "../utils/Types.sol"; + +/** + * @title ERC1155BalanceGteEnforcer + * @dev This contract enforces that the ERC1155 token balance of a recipient for a specific token ID + * has increased by at least the specified amount after the execution, measured between the `beforeHook` and `afterHook` calls. + */ +contract ERC1155BalanceGteEnforcer is CaveatEnforcer { + ////////////////////////////// State ////////////////////////////// + + mapping(bytes32 hashKey => uint256 balance) public balanceCache; + mapping(bytes32 hashKey => bool lock) public isLocked; + + ////////////////////////////// External Methods ////////////////////////////// + + /** + * @notice Generates the key that identifies the run. Produced by the hash of the values used. + * @param _caller Address of the sender calling the enforcer. + * @param _token ERC1155 token being compared in the beforeHook and afterHook. + * @param _recipient The address of the recipient of the token. + * @param _tokenId The ID of the ERC1155 token. + * @param _delegationHash The hash of the delegation. + * @return The hash to be used as key of the mapping. + */ + function getHashKey( + address _caller, + address _token, + address _recipient, + uint256 _tokenId, + bytes32 _delegationHash + ) + external + pure + returns (bytes32) + { + return _getHashKey(_caller, _token, _recipient, _tokenId, _delegationHash); + } + + ////////////////////////////// Public Methods ////////////////////////////// + + /** + * @notice This function caches the recipient's ERC1155 token balance before the delegation is executed. + * @param _terms 104 bytes where: + * - first 20 bytes: address of the ERC1155 token, + * - next 20 bytes: address of the recipient, + * - next 32 bytes: token ID, + * - next 32 bytes: amount the balance should increase by. + * @param _delegationHash The hash of the delegation. + */ + function beforeHook( + bytes calldata _terms, + bytes calldata, + ModeCode, + bytes calldata, + bytes32 _delegationHash, + address, + address + ) + public + override + { + (address token_, address recipient_, uint256 tokenId_,) = getTermsInfo(_terms); + bytes32 hashKey_ = _getHashKey(msg.sender, token_, recipient_, tokenId_, _delegationHash); + require(!isLocked[hashKey_], "ERC1155BalanceGteEnforcer:enforcer-is-locked"); + isLocked[hashKey_] = true; + uint256 balance_ = IERC1155(token_).balanceOf(recipient_, tokenId_); + balanceCache[hashKey_] = balance_; + } + + /** + * @notice This function enforces that the recipient's ERC1155 token balance has increased by at least the amount provided. + * @param _terms 104 bytes where: + * - first 20 bytes: address of the ERC1155 token, + * - next 20 bytes: address of the recipient, + * - next 32 bytes: token ID, + * - next 32 bytes: amount the balance should increase by. + */ + function afterHook( + bytes calldata _terms, + bytes calldata, + ModeCode, + bytes calldata, + bytes32 _delegationHash, + address, + address + ) + public + override + { + (address token_, address recipient_, uint256 tokenId_, uint256 amount_) = getTermsInfo(_terms); + bytes32 hashKey_ = _getHashKey(msg.sender, token_, recipient_, tokenId_, _delegationHash); + delete isLocked[hashKey_]; + uint256 balance_ = IERC1155(token_).balanceOf(recipient_, tokenId_); + require(balance_ >= balanceCache[hashKey_] + amount_, "ERC1155BalanceGteEnforcer:balance-not-gt"); + } + + /** + * @notice Decodes the terms used in this CaveatEnforcer. + * @param _terms Encoded data that is used during the execution hooks. + * @return token_ The address of the ERC1155 token. + * @return recipient_ The address of the recipient of the token. + * @return tokenId_ The ID of the ERC1155 token. + * @return amount_ The amount the balance should increase by. + */ + function getTermsInfo(bytes calldata _terms) + public + pure + returns (address token_, address recipient_, uint256 tokenId_, uint256 amount_) + { + require(_terms.length == 104, "ERC1155BalanceGteEnforcer:invalid-terms-length"); + token_ = address(bytes20(_terms[:20])); + recipient_ = address(bytes20(_terms[20:40])); + tokenId_ = uint256(bytes32(_terms[40:72])); + amount_ = uint256(bytes32(_terms[72:])); + } + + ////////////////////////////// Internal Methods ////////////////////////////// + + /** + * @notice Generates the key that identifies the run. Produced by hashing the provided values. + */ + function _getHashKey( + address _caller, + address _token, + address _recipient, + uint256 _tokenId, + bytes32 _delegationHash + ) + private + pure + returns (bytes32) + { + return keccak256(abi.encode(_caller, _token, _recipient, _tokenId, _delegationHash)); + } +} diff --git a/src/enforcers/ERC721BalanceGteEnforcer.sol b/src/enforcers/ERC721BalanceGteEnforcer.sol new file mode 100644 index 0000000..90d387a --- /dev/null +++ b/src/enforcers/ERC721BalanceGteEnforcer.sol @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; + +import { CaveatEnforcer } from "./CaveatEnforcer.sol"; +import { ModeCode } from "../utils/Types.sol"; + +/** + * @title ERC721BalanceGteEnforcer + * @dev This contract enforces that the ERC721 token balance of a recipient has increased by at least the specified amount + * after the execution, measured between the `beforeHook` and `afterHook` calls, regardless of what the execution is. + */ +contract ERC721BalanceGteEnforcer is CaveatEnforcer { + ////////////////////////////// State ////////////////////////////// + + mapping(bytes32 hashKey => uint256 balance) public balanceCache; + mapping(bytes32 hashKey => bool lock) public isLocked; + + ////////////////////////////// External Methods ////////////////////////////// + + /** + * @notice Generates the key that identifies the run. Produced by the hash of the values used. + * @param _caller Address of the sender calling the enforcer. + * @param _token ERC721 token being compared in the beforeHook and afterHook. + * @param _recipient The address of the recipient of the token. + * @param _delegationHash The hash of the delegation. + * @return The hash to be used as key of the mapping. + */ + function getHashKey( + address _caller, + address _token, + address _recipient, + bytes32 _delegationHash + ) + external + pure + returns (bytes32) + { + return _getHashKey(_caller, _token, _recipient, _delegationHash); + } + + ////////////////////////////// Public Methods ////////////////////////////// + + /** + * @notice This function caches the delegator's ERC721 token balance before the delegation is executed. + * @param _terms 72 bytes where: + * - first 20 bytes: address of the ERC721 token, + * - next 20 bytes: address of the recipient, + * - next 32 bytes: amount the balance should increase by. + * @param _delegationHash The hash of the delegation. + */ + function beforeHook( + bytes calldata _terms, + bytes calldata, + ModeCode, + bytes calldata, + bytes32 _delegationHash, + address, + address + ) + public + override + { + (address token_, address recipient_,) = getTermsInfo(_terms); + bytes32 hashKey_ = _getHashKey(msg.sender, token_, recipient_, _delegationHash); + require(!isLocked[hashKey_], "ERC721BalanceGteEnforcer:enforcer-is-locked"); + isLocked[hashKey_] = true; + uint256 balance_ = IERC721(token_).balanceOf(recipient_); + balanceCache[hashKey_] = balance_; + } + + /** + * @notice This function enforces that the delegator's ERC721 token balance has increased by at least the amount provided. + * @param _terms 72 bytes where: + * - first 20 bytes: address of the ERC721 token, + * - next 20 bytes: address of the recipient, + * - next 32 bytes: amount the balance should increase by. + */ + function afterHook( + bytes calldata _terms, + bytes calldata, + ModeCode, + bytes calldata, + bytes32 _delegationHash, + address, + address + ) + public + override + { + (address token_, address recipient_, uint256 amount_) = getTermsInfo(_terms); + bytes32 hashKey_ = _getHashKey(msg.sender, token_, recipient_, _delegationHash); + delete isLocked[hashKey_]; + uint256 balance_ = IERC721(token_).balanceOf(recipient_); + require(balance_ >= balanceCache[hashKey_] + amount_, "ERC721BalanceGteEnforcer:balance-not-gt"); + } + + /** + * @notice Decodes the terms used in this CaveatEnforcer. + * @param _terms Encoded data that is used during the execution hooks. + * @return token_ The address of the ERC721 token. + * @return recipient_ The address of the recipient of the token. + * @return amount_ The amount the balance should increase by. + */ + function getTermsInfo(bytes calldata _terms) public pure returns (address token_, address recipient_, uint256 amount_) { + require(_terms.length == 72, "ERC721BalanceGteEnforcer:invalid-terms-length"); + token_ = address(bytes20(_terms[:20])); + recipient_ = address(bytes20(_terms[20:40])); + amount_ = uint256(bytes32(_terms[40:])); + } + + ////////////////////////////// Internal Methods ////////////////////////////// + + /** + * @notice Generates the key that identifies the run. Produced by the hash of the values used. + */ + function _getHashKey( + address _caller, + address _token, + address _recipient, + bytes32 _delegationHash + ) + private + pure + returns (bytes32) + { + return keccak256(abi.encode(_caller, _token, _recipient, _delegationHash)); + } +} diff --git a/test/enforcers/ERC1155BalanceGteEnforcer.t.sol b/test/enforcers/ERC1155BalanceGteEnforcer.t.sol new file mode 100644 index 0000000..9534e47 --- /dev/null +++ b/test/enforcers/ERC1155BalanceGteEnforcer.t.sol @@ -0,0 +1,267 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import "forge-std/Test.sol"; +import { BasicERC1155 } from "../utils/BasicERC1155.t.sol"; +import { ModeLib } from "@erc7579/lib/ModeLib.sol"; + +import "../../src/utils/Types.sol"; +import { Execution } from "../../src/utils/Types.sol"; +import { CaveatEnforcerBaseTest } from "./CaveatEnforcerBaseTest.t.sol"; +import { ERC1155BalanceGteEnforcer } from "../../src/enforcers/ERC1155BalanceGteEnforcer.sol"; +import { ICaveatEnforcer } from "../../src/interfaces/ICaveatEnforcer.sol"; + +contract ERC1155BalanceGteEnforcerTest is CaveatEnforcerBaseTest { + ////////////////////////////// State ////////////////////////////// + ERC1155BalanceGteEnforcer public enforcer; + BasicERC1155 public token; + address delegator; + address delegate; + address dm; + Execution mintExecution; + bytes mintExecutionCallData; + ModeCode public mode = ModeLib.encodeSimpleSingle(); + + uint256 public tokenId = 1; + + ////////////////////// Set up ////////////////////// + + function setUp() public override { + super.setUp(); + delegator = address(users.alice.deleGator); + delegate = address(users.bob.deleGator); + dm = address(delegationManager); + enforcer = new ERC1155BalanceGteEnforcer(); + vm.label(address(enforcer), "ERC1155 BalanceGte Enforcer"); + token = new BasicERC1155(delegator, "ERC1155Token", "ERC1155Token", ""); + vm.label(address(token), "ERC1155 Test Token"); + + // Prepare the Execution data for minting + mintExecution = Execution({ + target: address(token), + value: 0, + callData: abi.encodeWithSelector(token.mint.selector, delegator, tokenId, 100, "") + }); + mintExecutionCallData = abi.encode(mintExecution); + } + + ////////////////////// Basic Functionality ////////////////////// + + // Validates the terms get decoded correctly + function test_decodedTheTerms() public { + bytes memory terms_ = abi.encodePacked(address(token), address(delegator), uint256(tokenId), uint256(100)); + (address token_, address recipient_, uint256 tokenId_, uint256 amount_) = enforcer.getTermsInfo(terms_); + assertEq(token_, address(token)); + assertEq(recipient_, delegator); + assertEq(tokenId_, tokenId); + assertEq(amount_, 100); + } + + // Validates that a balance has increased at least by the expected amount + function test_allow_ifBalanceIncreases() public { + // Expect it to increase by at least 100 + bytes memory terms_ = abi.encodePacked(address(token), address(delegator), uint256(tokenId), uint256(100)); + + // Increase by 100 + vm.prank(dm); + enforcer.beforeHook(terms_, hex"", mode, mintExecutionCallData, bytes32(0), address(0), delegate); + vm.prank(delegator); + token.mint(delegator, tokenId, 100, ""); + vm.prank(dm); + enforcer.afterHook(terms_, hex"", mode, mintExecutionCallData, bytes32(0), address(0), delegate); + + // Increase by 1000 + vm.prank(dm); + enforcer.beforeHook(terms_, hex"", mode, mintExecutionCallData, bytes32(0), address(0), delegate); + vm.prank(delegator); + token.mint(delegator, tokenId, 1000, ""); + vm.prank(dm); + enforcer.afterHook(terms_, hex"", mode, mintExecutionCallData, bytes32(0), address(0), delegate); + } + + ////////////////////// Errors ////////////////////// + + // Reverts if a balance hasn't increased by the set amount + function test_notAllow_insufficientIncrease() public { + // Expect it to increase by at least 100 + bytes memory terms_ = abi.encodePacked(address(token), address(delegator), uint256(tokenId), uint256(100)); + + // Increase by 10, expect revert + vm.prank(dm); + enforcer.beforeHook(terms_, hex"", mode, mintExecutionCallData, bytes32(0), address(0), delegate); + vm.prank(delegator); + token.mint(delegator, tokenId, 10, ""); + vm.prank(dm); + vm.expectRevert(bytes("ERC1155BalanceGteEnforcer:balance-not-gt")); + enforcer.afterHook(terms_, hex"", mode, mintExecutionCallData, bytes32(0), address(0), delegate); + } + + // Reverts if a balance descreased in between the hooks + function test_notAllow_ifBalanceDecreases() public { + // Starting with 10 tokens + vm.prank(delegator); + token.mint(delegator, tokenId, 10, ""); + + // Expect it to increase by at least 100 + bytes memory terms_ = abi.encodePacked(address(token), address(delegator), uint256(tokenId), uint256(100)); + + vm.prank(dm); + enforcer.beforeHook(terms_, hex"", mode, mintExecutionCallData, bytes32(0), address(0), delegate); + + // Decrease balance by transferring tokens away + vm.prank(delegator); + token.safeTransferFrom(delegator, address(1), tokenId, 10, ""); + + vm.prank(dm); + vm.expectRevert(bytes("ERC1155BalanceGteEnforcer:balance-not-gt")); + enforcer.afterHook(terms_, hex"", mode, mintExecutionCallData, bytes32(0), address(0), delegate); + } + + // Allows to check the balance of different recipients + function test_allow_withDifferentRecipients() public { + address[] memory recipients_ = new address[](2); + recipients_[0] = delegator; + recipients_[1] = address(99999); + + for (uint256 i = 0; i < recipients_.length; i++) { + address currentRecipient_ = recipients_[i]; + bytes memory terms_ = abi.encodePacked(address(token), currentRecipient_, uint256(tokenId), uint256(100)); + + // Increase by 100 for each recipient + vm.prank(dm); + enforcer.beforeHook(terms_, hex"", mode, mintExecutionCallData, bytes32(i), address(0), delegate); + vm.prank(delegator); + token.mint(currentRecipient_, tokenId, 100, ""); + vm.prank(dm); + enforcer.afterHook(terms_, hex"", mode, mintExecutionCallData, bytes32(i), address(0), delegate); + } + } + + // Considers any pre existing balances in the recipient + function test_notAllow_withPreExistingBalance() public { + // Recipient already has 50 tokens + vm.prank(delegator); + token.mint(delegator, tokenId, 50, ""); + + // Expect balance to increase by at least 100 + bytes memory terms_ = abi.encodePacked(address(token), address(delegator), uint256(tokenId), uint256(100)); + + vm.prank(dm); + enforcer.beforeHook(terms_, hex"", mode, mintExecutionCallData, bytes32(0), address(0), delegate); + + // Increase balance by 100 + vm.prank(delegator); + token.mint(delegator, tokenId, 50, ""); + + vm.prank(dm); + vm.expectRevert(bytes("ERC1155BalanceGteEnforcer:balance-not-gt")); + enforcer.afterHook(terms_, hex"", mode, mintExecutionCallData, bytes32(0), address(0), delegate); + } + + // Same delegation hash multiple recipients + function test_differentiateDelegationHashWithRecipient() public { + bytes32 delegationHash_ = bytes32(bytes32(uint256(99999999))); + address recipient2_ = address(1111111); + // Expect balance to increase by at least 100 in different recipients + bytes memory terms1_ = abi.encodePacked(address(token), address(delegator), uint256(tokenId), uint256(100)); + bytes memory terms2_ = abi.encodePacked(address(token), address(recipient2_), uint256(tokenId), uint256(100)); + + vm.prank(dm); + enforcer.beforeHook(terms1_, hex"", mode, mintExecutionCallData, delegationHash_, address(0), delegate); + + vm.prank(dm); + enforcer.beforeHook(terms2_, hex"", mode, mintExecutionCallData, delegationHash_, address(0), delegate); + + // Increase balance by 100 only in recipient1 + vm.prank(delegator); + token.mint(delegator, tokenId, 100, ""); + + // This one works well recipient1 increased + vm.prank(dm); + enforcer.afterHook(terms1_, hex"", mode, mintExecutionCallData, delegationHash_, address(0), delegate); + + // This one fails recipient1 didn't increase + vm.prank(dm); + vm.expectRevert(bytes("ERC1155BalanceGteEnforcer:balance-not-gt")); + enforcer.afterHook(terms2_, hex"", mode, mintExecutionCallData, delegationHash_, address(0), delegate); + + // Increase balance by 100 only in recipient2 to fix it + vm.prank(delegator); + token.mint(recipient2_, tokenId, 100, ""); + + // Recipient2 works well + vm.prank(dm); + enforcer.afterHook(terms2_, hex"", mode, mintExecutionCallData, delegationHash_, address(0), delegate); + } + + // Reverts if the enforcer is locked + function test_notAllow_reenterALockedEnforcer() public { + // Expect it to increase by at least 100 + bytes memory terms_ = abi.encodePacked(address(token), address(delegator), uint256(tokenId), uint256(100)); + bytes32 delegationHash_ = bytes32(uint256(99999999)); + + // Lock the enforcer + vm.startPrank(dm); + enforcer.beforeHook(terms_, hex"", mode, mintExecutionCallData, delegationHash_, address(0), delegate); + bytes32 hashKey_ = + enforcer.getHashKey(address(delegationManager), address(token), address(delegator), tokenId, delegationHash_); + assertTrue(enforcer.isLocked(hashKey_)); + vm.expectRevert(bytes("ERC1155BalanceGteEnforcer:enforcer-is-locked")); + enforcer.beforeHook(terms_, hex"", mode, mintExecutionCallData, delegationHash_, address(0), delegate); + vm.stopPrank(); + + vm.prank(delegator); + token.mint(delegator, tokenId, 1000, ""); + + vm.startPrank(dm); + // Unlock the enforcer + enforcer.afterHook(terms_, hex"", mode, mintExecutionCallData, delegationHash_, address(0), delegate); + assertFalse(enforcer.isLocked(hashKey_)); + // Can be used again, and locks it again + enforcer.beforeHook(terms_, hex"", mode, mintExecutionCallData, delegationHash_, address(0), delegate); + assertTrue(enforcer.isLocked(hashKey_)); + vm.stopPrank(); + } + + // Validates the terms are well-formed + function test_invalid_decodedTheTerms() public { + bytes memory terms_; + + // Too small + terms_ = abi.encodePacked(address(token), address(delegator), uint8(100)); + vm.expectRevert(bytes("ERC1155BalanceGteEnforcer:invalid-terms-length")); + enforcer.getTermsInfo(terms_); + + // Too large + terms_ = abi.encodePacked(uint256(100), uint256(100), uint256(100), uint256(100)); + vm.expectRevert(bytes("ERC1155BalanceGteEnforcer:invalid-terms-length")); + enforcer.getTermsInfo(terms_); + } + + // Validates that an invalid token address causes a revert + function test_invalid_tokenAddress() public { + bytes memory terms_; + + // Invalid token address + terms_ = abi.encodePacked(address(0), address(delegator), uint256(tokenId), uint256(100)); + vm.expectRevert(); + enforcer.beforeHook(terms_, hex"", mode, mintExecutionCallData, bytes32(0), address(0), delegate); + } + + // Validates that an invalid amount causes a revert + function test_notAllow_expectingOverflow() public { + // Expect balance to increase by max uint256, which is unrealistic + bytes memory terms_ = abi.encodePacked(address(token), address(delegator), uint256(tokenId), type(uint256).max); + + vm.prank(dm); + enforcer.beforeHook(terms_, hex"", mode, mintExecutionCallData, bytes32(0), address(0), delegate); + vm.expectRevert(); + enforcer.afterHook(terms_, hex"", mode, mintExecutionCallData, bytes32(0), address(0), delegate); + } + + ////////////////////// Integration ////////////////////// + + function _getEnforcer() internal view override returns (ICaveatEnforcer) { + return ICaveatEnforcer(address(enforcer)); + } +} diff --git a/test/enforcers/ERC20BalanceGteEnforcer.t.sol b/test/enforcers/ERC20BalanceGteEnforcer.t.sol index 48362e8..84314a5 100644 --- a/test/enforcers/ERC20BalanceGteEnforcer.t.sol +++ b/test/enforcers/ERC20BalanceGteEnforcer.t.sol @@ -12,8 +12,6 @@ import { ERC20BalanceGteEnforcer } from "../../src/enforcers/ERC20BalanceGteEnfo import { ICaveatEnforcer } from "../../src/interfaces/ICaveatEnforcer.sol"; contract ERC20BalanceGteEnforcerTest is CaveatEnforcerBaseTest { - using ModeLib for ModeCode; - ////////////////////////////// State ////////////////////////////// ERC20BalanceGteEnforcer public enforcer; BasicERC20 public token; diff --git a/test/enforcers/ERC721BalanceGteEnforcer.t.sol b/test/enforcers/ERC721BalanceGteEnforcer.t.sol new file mode 100644 index 0000000..b034472 --- /dev/null +++ b/test/enforcers/ERC721BalanceGteEnforcer.t.sol @@ -0,0 +1,258 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import "forge-std/Test.sol"; +import { BasicCF721 } from "../utils/BasicCF721.t.sol"; +import { ModeLib } from "@erc7579/lib/ModeLib.sol"; + +import "../../src/utils/Types.sol"; +import { Execution } from "../../src/utils/Types.sol"; +import { CaveatEnforcerBaseTest } from "./CaveatEnforcerBaseTest.t.sol"; +import { ERC721BalanceGteEnforcer } from "../../src/enforcers/ERC721BalanceGteEnforcer.sol"; +import { ICaveatEnforcer } from "../../src/interfaces/ICaveatEnforcer.sol"; + +contract ERC721BalanceGteEnforcerTest is CaveatEnforcerBaseTest { + ////////////////////////////// State ////////////////////////////// + ERC721BalanceGteEnforcer public enforcer; + BasicCF721 public token; + address delegator; + address delegate; + address dm; + Execution mintExecution; + bytes mintExecutionCallData; + ModeCode public mode = ModeLib.encodeSimpleSingle(); + + ////////////////////// Set up ////////////////////// + + function setUp() public override { + super.setUp(); + delegator = address(users.alice.deleGator); + delegate = address(users.bob.deleGator); + dm = address(delegationManager); + enforcer = new ERC721BalanceGteEnforcer(); + vm.label(address(enforcer), "ERC721 BalanceGte Enforcer"); + token = new BasicCF721(delegator, "ERC721Token", "ERC721Token", ""); + vm.label(address(token), "ERC721 Test Token"); + + // Prepare the Execution data for minting + mintExecution = + Execution({ target: address(token), value: 0, callData: abi.encodeWithSelector(token.mint.selector, delegator) }); + mintExecutionCallData = abi.encode(mintExecution); + } + + ////////////////////// Basic Functionality ////////////////////// + + // Validates the terms get decoded correctly + function test_decodedTheTerms() public { + bytes memory terms_ = abi.encodePacked(address(token), address(delegator), uint256(1)); + (address token_, address recipient_, uint256 amount_) = enforcer.getTermsInfo(terms_); + assertEq(token_, address(token)); + assertEq(recipient_, delegator); + assertEq(amount_, 1); + } + + // Validates that a balance has increased at least by the expected amount + function test_allow_ifBalanceIncreases() public { + // Expect it to increase by at least 1 + bytes memory terms_ = abi.encodePacked(address(token), address(delegator), uint256(1)); + + // Increase by 1 + vm.prank(dm); + enforcer.beforeHook(terms_, hex"", mode, mintExecutionCallData, bytes32(0), address(0), delegate); + vm.prank(delegator); + token.mint(delegator); + vm.prank(dm); + enforcer.afterHook(terms_, hex"", mode, mintExecutionCallData, bytes32(0), address(0), delegate); + + // Increase by 2 + vm.prank(dm); + enforcer.beforeHook(terms_, hex"", mode, mintExecutionCallData, bytes32(0), address(0), delegate); + vm.prank(delegator); + token.mint(delegator); + vm.prank(dm); + enforcer.afterHook(terms_, hex"", mode, mintExecutionCallData, bytes32(0), address(0), delegate); + } + + ////////////////////// Errors ////////////////////// + + // Reverts if a balance hasn't increased by the set amount + function test_notAllow_insufficientIncrease() public { + // Expect it to increase by at least 1 + bytes memory terms_ = abi.encodePacked(address(token), address(delegator), uint256(1)); + + // No increase + vm.prank(dm); + enforcer.beforeHook(terms_, hex"", mode, mintExecutionCallData, bytes32(0), address(0), delegate); + // No minting occurs here + vm.prank(dm); + vm.expectRevert(bytes("ERC721BalanceGteEnforcer:balance-not-gt")); + enforcer.afterHook(terms_, hex"", mode, mintExecutionCallData, bytes32(0), address(0), delegate); + } + + // Reverts if a balance decreased in between the hooks + function test_notAllow_ifBalanceDecreases() public { + // Starting with 1 token + vm.prank(delegator); + token.mint(delegator); + + // Expect it to increase by at least 1 + bytes memory terms_ = abi.encodePacked(address(token), address(delegator), uint256(1)); + + vm.prank(dm); + enforcer.beforeHook(terms_, hex"", mode, mintExecutionCallData, bytes32(0), address(0), delegate); + + // Decrease balance by transferring token away + uint256 tokenIdToTransfer_ = (token.tokenId()) - 1; + vm.prank(delegator); + token.transferFrom(delegator, address(1), tokenIdToTransfer_); + + vm.prank(dm); + vm.expectRevert(bytes("ERC721BalanceGteEnforcer:balance-not-gt")); + enforcer.afterHook(terms_, hex"", mode, mintExecutionCallData, bytes32(0), address(0), delegate); + } + + // Allows to check the balance of different recipients + function test_allow_withDifferentRecipients() public { + address[] memory recipients_ = new address[](2); + recipients_[0] = delegator; + recipients_[1] = address(99999); + + for (uint256 i = 0; i < recipients_.length; i++) { + address currentRecipient_ = recipients_[i]; + bytes memory terms_ = abi.encodePacked(address(token), currentRecipient_, uint256(1)); + + // Increase by 1 for each recipient + vm.prank(dm); + enforcer.beforeHook(terms_, hex"", mode, mintExecutionCallData, bytes32(i), address(0), delegate); + vm.prank(delegator); + token.mint(currentRecipient_); + vm.prank(dm); + enforcer.afterHook(terms_, hex"", mode, mintExecutionCallData, bytes32(i), address(0), delegate); + } + } + + // Considers any pre-existing balances in the recipient + function test_notAllow_withPreExistingBalance() public { + // Recipient already has 1 token + vm.prank(delegator); + token.mint(delegator); + + // Expect balance to increase by at least 1 + bytes memory terms_ = abi.encodePacked(address(token), address(delegator), uint256(1)); + + vm.prank(dm); + enforcer.beforeHook(terms_, hex"", mode, mintExecutionCallData, bytes32(0), address(0), delegate); + + // No increase occurs here + + vm.prank(dm); + vm.expectRevert(bytes("ERC721BalanceGteEnforcer:balance-not-gt")); + enforcer.afterHook(terms_, hex"", mode, mintExecutionCallData, bytes32(0), address(0), delegate); + } + + // Same delegation hash multiple recipients + function test_differentiateDelegationHashWithRecipient() public { + bytes32 delegationHash_ = bytes32(uint256(99999999)); + address recipient2_ = address(1111111); + // Expect balance to increase by at least 1 in different recipients + bytes memory terms1_ = abi.encodePacked(address(token), delegator, uint256(1)); + bytes memory terms2_ = abi.encodePacked(address(token), recipient2_, uint256(1)); + + vm.prank(dm); + enforcer.beforeHook(terms1_, hex"", mode, mintExecutionCallData, delegationHash_, address(0), delegate); + + vm.prank(dm); + enforcer.beforeHook(terms2_, hex"", mode, mintExecutionCallData, delegationHash_, address(0), delegate); + + // Increase balance by 1 only in recipient1 + vm.prank(delegator); + token.mint(delegator); + + // This one works well recipient1 increased + vm.prank(dm); + enforcer.afterHook(terms1_, hex"", mode, mintExecutionCallData, delegationHash_, address(0), delegate); + + // This one fails recipient2 didn't increase + vm.prank(dm); + vm.expectRevert(bytes("ERC721BalanceGteEnforcer:balance-not-gt")); + enforcer.afterHook(terms2_, hex"", mode, mintExecutionCallData, delegationHash_, address(0), delegate); + + // Increase balance by 1 in recipient2 to fix it + vm.prank(delegator); + token.mint(recipient2_); + + // Recipient2 works well + vm.prank(dm); + enforcer.afterHook(terms2_, hex"", mode, mintExecutionCallData, delegationHash_, address(0), delegate); + } + + // Reverts if the enforcer is locked + function test_notAllow_reenterALockedEnforcer() public { + // Expect it to increase by at least 1 + bytes memory terms_ = abi.encodePacked(address(token), address(delegator), uint256(1)); + bytes32 delegationHash_ = bytes32(uint256(99999999)); + + // Lock the enforcer + vm.startPrank(dm); + enforcer.beforeHook(terms_, hex"", mode, mintExecutionCallData, delegationHash_, address(0), delegate); + bytes32 hashKey_ = enforcer.getHashKey(address(delegationManager), address(token), address(delegator), delegationHash_); + assertTrue(enforcer.isLocked(hashKey_)); + vm.expectRevert(bytes("ERC721BalanceGteEnforcer:enforcer-is-locked")); + enforcer.beforeHook(terms_, hex"", mode, mintExecutionCallData, delegationHash_, address(0), delegate); + vm.stopPrank(); + + vm.prank(delegator); + token.mint(delegator); + + vm.startPrank(dm); + // Unlock the enforcer + enforcer.afterHook(terms_, hex"", mode, mintExecutionCallData, delegationHash_, address(0), delegate); + assertFalse(enforcer.isLocked(hashKey_)); + // Can be used again, and locks it again + enforcer.beforeHook(terms_, hex"", mode, mintExecutionCallData, delegationHash_, address(0), delegate); + assertTrue(enforcer.isLocked(hashKey_)); + vm.stopPrank(); + } + + // Validates the terms are well-formed + function test_invalid_decodedTheTerms() public { + bytes memory terms_; + + // Too small + terms_ = abi.encodePacked(address(token), address(delegator), uint8(1)); + vm.expectRevert(bytes("ERC721BalanceGteEnforcer:invalid-terms-length")); + enforcer.getTermsInfo(terms_); + + // Too large + terms_ = abi.encodePacked(uint256(1), uint256(1), uint256(1), uint256(1)); + vm.expectRevert(bytes("ERC721BalanceGteEnforcer:invalid-terms-length")); + enforcer.getTermsInfo(terms_); + } + + // Validates that an invalid token address causes a revert + function test_invalid_tokenAddress() public { + bytes memory terms_; + + // Invalid token address + terms_ = abi.encodePacked(address(0), address(delegator), uint256(1)); + vm.expectRevert(); + enforcer.beforeHook(terms_, hex"", mode, mintExecutionCallData, bytes32(0), address(0), delegate); + } + + // Validates that an unrealistic amount causes a revert + function test_notAllow_expectingOverflow() public { + // Expect balance to increase by max uint256, which is unrealistic + bytes memory terms_ = abi.encodePacked(address(token), address(delegator), type(uint256).max); + + vm.prank(dm); + enforcer.beforeHook(terms_, hex"", mode, mintExecutionCallData, bytes32(0), address(0), delegate); + vm.expectRevert(); + enforcer.afterHook(terms_, hex"", mode, mintExecutionCallData, bytes32(0), address(0), delegate); + } + + ////////////////////// Integration ////////////////////// + + function _getEnforcer() internal view override returns (ICaveatEnforcer) { + return ICaveatEnforcer(address(enforcer)); + } +} diff --git a/test/utils/BasicERC1155.t.sol b/test/utils/BasicERC1155.t.sol new file mode 100644 index 0000000..8b05dd4 --- /dev/null +++ b/test/utils/BasicERC1155.t.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 + +pragma solidity 0.8.23; + +import { ERC1155 } from "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; +import { Ownable2Step, Ownable } from "@openzeppelin/contracts/access/Ownable2Step.sol"; + +contract BasicERC1155 is ERC1155, Ownable2Step { + ////////////////////////////// State ////////////////////////////// + + uint256 public tokenId; + + ////////////////////////////// Constructor ////////////////////////////// + + /// @dev Initializes the BasicERC1155 contract. + /// @param _owner The owner of the ERC20 token. Also addres that received the initial amount of tokens. + /// @param _name The name of the ERC20 token. + /// @param _symbol The symbol of the ERC20 token. + /// @param _baseUri The base URI of the tokens. + constructor( + address _owner, + string memory _name, + string memory _symbol, + string memory _baseUri + ) + Ownable(_owner) + ERC1155(_baseUri) + { } + + ////////////////////////////// External Methods ////////////////////////////// + + /// @dev Allows the onwner to burn tokens from the specified user. + /// @param _from The address of the user from whom the tokens will be burned. + /// @param _id The token id to burn. + /// @param _value The amount of tokens to burn. + function burn(address _from, uint256 _id, uint256 _value) external onlyOwner { + _burn(_from, _id, _value); + } + + /// @dev Allows the owner to mint new tokens and assigns them to the specified user. + /// @param _to The address of the user to whom the tokens will be minted. + /// @param _id The token id to mint. + /// @param _value The amount of tokens to mint. + /// @param _data Any data related to the token. + function mint(address _to, uint256 _id, uint256 _value, bytes memory _data) external onlyOwner { + _mint(_to, _id, _value, _data); + } +} diff --git a/test/utils/BasicERC20.t.sol b/test/utils/BasicERC20.t.sol index 2f989d2..5c47156 100644 --- a/test/utils/BasicERC20.t.sol +++ b/test/utils/BasicERC20.t.sol @@ -5,7 +5,7 @@ pragma solidity 0.8.23; import { ERC20, IERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { Ownable2Step, Ownable } from "@openzeppelin/contracts/access/Ownable2Step.sol"; -contract BasicERC20 is ERC20, Ownable { +contract BasicERC20 is ERC20, Ownable2Step { ////////////////////////////// Constructor ////////////////////////////// /// @dev Initializes the BasicERC20 contract.