From ac5a6396e043e218cee2f4e49eaa1986507c9d5d Mon Sep 17 00:00:00 2001 From: Kevin Cheng Date: Fri, 20 Oct 2023 11:40:23 -0700 Subject: [PATCH] Add permit and encumberBySig (#18) Implements `permit` and `encumberBySig`, both of which support EIP-1271 signatures. --- .gitmodules | 3 + lib/openzeppelin-contracts | 1 + remappings.txt | 7 +- src/CometWrapper.sol | 140 +++++ src/test/EIP1271Signer.sol | 65 ++ test/BaseUSDbCTest.t.sol | 3 +- test/BySig.t.sol | 1140 ++++++++++++++++++++++++++++++++++++ test/CoreTest.sol | 7 +- test/MainnetUSDCTest.t.sol | 3 +- test/MainnetWETHTest.t.sol | 3 +- 10 files changed, 1366 insertions(+), 6 deletions(-) create mode 160000 lib/openzeppelin-contracts create mode 100644 src/test/EIP1271Signer.sol create mode 100644 test/BySig.t.sol diff --git a/.gitmodules b/.gitmodules index 0dab6dd..ed4e5ba 100644 --- a/.gitmodules +++ b/.gitmodules @@ -6,3 +6,6 @@ path = lib/solmate url = https://github.com/transmissions11/solmate branch = v7 +[submodule "lib/openzeppelin-contracts"] + path = lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts new file mode 160000 index 0000000..fd81a96 --- /dev/null +++ b/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit fd81a96f01cc42ef1c9a5399364968d0e07e9e90 diff --git a/remappings.txt b/remappings.txt index 4e8c946..97fbd87 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,3 +1,6 @@ -ds-test/=lib/forge-std/lib/ds-test/src/ forge-std/=lib/forge-std/src/ -solmate/=lib/solmate/src/ \ No newline at end of file +ds-test/=lib/forge-std/lib/ds-test/src/ +solmate/=lib/solmate/src/ +erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/ +openzeppelin-contracts/=lib/openzeppelin-contracts/ +openzeppelin/=lib/openzeppelin-contracts/contracts/ diff --git a/src/CometWrapper.sol b/src/CometWrapper.sol index 5bb9cc2..effc087 100644 --- a/src/CometWrapper.sol +++ b/src/CometWrapper.sol @@ -8,6 +8,7 @@ import { CometInterface, TotalsBasic } from "./vendor/CometInterface.sol"; import { CometHelpers } from "./CometHelpers.sol"; import { ICometRewards } from "./vendor/ICometRewards.sol"; import { IERC7246 } from "./vendor/IERC7246.sol"; +import { ECDSA } from "openzeppelin/utils/cryptography/ECDSA.sol"; /** * @title Comet Wrapper @@ -22,6 +23,23 @@ contract CometWrapper is ERC4626, IERC7246, CometHelpers { uint64 baseTrackingIndex; } + /// @notice The major version of this contract + string public constant VERSION = "1"; + + /// @dev The EIP-712 typehash for authorization via permit + bytes32 internal constant AUTHORIZATION_TYPEHASH = keccak256("Authorization(address owner,address spender,uint256 amount,uint256 nonce,uint256 expiry)"); + + /// @dev The EIP-712 typehash for encumber via encumberBySig + bytes32 internal constant ENCUMBER_TYPEHASH = keccak256("Encumber(address owner,address taker,uint256 amount,uint256 nonce,uint256 expiry)"); + + /// @dev The EIP-712 typehash for the contract's domain + bytes32 internal constant DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + + /// @dev The magic value that a contract's `isValidSignature(bytes32 hash, bytes signature)` function should + /// return for a valid signature + /// See https://eips.ethereum.org/EIPS/eip-1271 + bytes4 internal constant EIP1271_MAGIC_VALUE = 0x1626ba7e; + /// @notice Mapping of users to basic data mapping(address => UserBasic) public userBasic; @@ -48,9 +66,13 @@ contract CometWrapper is ERC4626, IERC7246, CometHelpers { /** Custom errors **/ + error BadSignatory(); + error EIP1271VerificationFailed(); error InsufficientAllowance(); error InsufficientAvailableBalance(); error InsufficientEncumbrance(); + error InvalidSignatureS(); + error SignatureExpired(); error TimestampTooLarge(); error UninitializedReward(); error ZeroShares(); @@ -590,4 +612,122 @@ contract CometWrapper is ERC4626, IERC7246, CometHelpers { releaseEncumbranceInternal(owner, spender, amount); } } + + /** + * @notice Returns the domain separator used in the encoding of the signature for permit + * @return bytes32 The domain separator + */ + function DOMAIN_SEPARATOR() public view override returns (bytes32) { + return keccak256(abi.encode(DOMAIN_TYPEHASH, keccak256(bytes(name)), keccak256(bytes(VERSION)), block.chainid, address(this))); + } + + /** + * @notice Sets approval amount for a spender via signature from signatory + * @param owner The address that signed the signature + * @param spender The address to authorize (or rescind authorization from) + * @param amount Amount that `owner` is approving for `spender` + * @param expiry Expiration time for the signature + * @param v The recovery byte of the signature + * @param r Half of the ECDSA signature pair + * @param s Half of the ECDSA signature pair + */ + function permit( + address owner, + address spender, + uint256 amount, + uint256 expiry, + uint8 v, + bytes32 r, + bytes32 s + ) public override { + if (block.timestamp >= expiry) revert SignatureExpired(); + + uint256 nonce = nonces[owner]; + bytes32 structHash = keccak256(abi.encode(AUTHORIZATION_TYPEHASH, owner, spender, amount, nonce, expiry)); + bytes32 digest = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR(), structHash)); + if (isValidSignature(owner, digest, v, r, s)) { + nonces[owner]++; + allowance[owner][spender] = amount; + emit Approval(owner, spender, amount); + } else { + revert BadSignatory(); + } + } + + /** + * @notice Sets an encumbrance from owner to taker via signature from signatory + * @param owner The address that signed the signature + * @param taker The address to create an encumbrance to + * @param amount Amount that owner is encumbering to taker + * @param expiry Expiration time for the signature + * @param v The recovery byte of the signature + * @param r Half of the ECDSA signature pair + * @param s Half of the ECDSA signature pair + */ + function encumberBySig( + address owner, + address taker, + uint256 amount, + uint256 expiry, + uint8 v, + bytes32 r, + bytes32 s + ) external { + if (block.timestamp >= expiry) revert SignatureExpired(); + + uint256 nonce = nonces[owner]; + bytes32 structHash = keccak256(abi.encode(ENCUMBER_TYPEHASH, owner, taker, amount, nonce, expiry)); + bytes32 digest = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR(), structHash)); + if (isValidSignature(owner, digest, v, r, s)) { + nonces[owner]++; + encumberInternal(owner, taker, amount); + } else { + revert BadSignatory(); + } + } + + /** + * @notice Checks if a signature is valid + * @dev Supports EIP-1271 signatures for smart contracts + * @param signer The address that signed the signature + * @param digest The hashed message that is signed + * @param v The recovery byte of the signature + * @param r Half of the ECDSA signature pair + * @param s Half of the ECDSA signature pair + * @return bool Whether the signature is valid + */ + function isValidSignature( + address signer, + bytes32 digest, + uint8 v, + bytes32 r, + bytes32 s + ) internal view returns (bool) { + if (hasCode(signer)) { + bytes memory signature = abi.encodePacked(r, s, v); + (bool success, bytes memory data) = signer.staticcall( + abi.encodeWithSelector(EIP1271_MAGIC_VALUE, digest, signature) + ); + if (success == false) revert EIP1271VerificationFailed(); + bytes4 returnValue = abi.decode(data, (bytes4)); + return returnValue == EIP1271_MAGIC_VALUE; + } else { + (address recoveredSigner, ECDSA.RecoverError recoverError) = ECDSA.tryRecover(digest, v, r, s); + if (recoverError == ECDSA.RecoverError.InvalidSignatureS) revert InvalidSignatureS(); + if (recoverError == ECDSA.RecoverError.InvalidSignature) revert BadSignatory(); + if (recoveredSigner != signer) revert BadSignatory(); + return true; + } + } + + /** + * @notice Checks if an address has code deployed to it + * @param addr The address to check + * @return bool Whether the address contains code + */ + function hasCode(address addr) internal view returns (bool) { + uint256 size; + assembly { size := extcodesize(addr) } + return size > 0; + } } diff --git a/src/test/EIP1271Signer.sol b/src/test/EIP1271Signer.sol new file mode 100644 index 0000000..ed4d320 --- /dev/null +++ b/src/test/EIP1271Signer.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.21; + +contract EIP1271Signer { + bytes4 internal constant EIP1271_MAGIC_VALUE = 0x1626ba7e; + + address public owner; + + constructor(address _owner) { + owner = _owner; + } + + function isValidSignature(bytes32 messageHash, bytes memory signature) external view returns (bytes4) { + if (recoverSigner(messageHash, signature) == owner) { + return EIP1271_MAGIC_VALUE; + } else { + return 0xffffffff; + } + } + + function recoverSigner(bytes32 messageHash, bytes memory signature) internal pure returns (address) { + require(signature.length == 65, "SignatureValidator#recoverSigner: invalid signature length"); + + bytes32 r; + bytes32 s; + uint8 v; + assembly { + r := mload(add(signature, 32)) + s := mload(add(signature, 64)) + v := and(mload(add(signature, 65)), 255) + } + + // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature + // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines + // the valid range for s in (281): 0 < s < secp256k1n ÷ 2 + 1, and for v in (282): v ∈ {27, 28}. Most + // signatures from current libraries generate a unique signature with an s-value in the lower half order. + // + // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value + // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or + // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept + // these malleable signatures as well. + // + // Source OpenZeppelin + // https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/ECDSA.sol + + if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) { + revert("SignatureValidator#recoverSigner: invalid signature 's' value"); + } + + if (v != 27 && v != 28) { + revert("SignatureValidator#recoverSigner: invalid signature 'v' value"); + } + + // Recover ECDSA signer + address signer = ecrecover(messageHash, v, r, s); + + // Prevent signer from being 0x0 + require( + signer != address(0x0), + "SignatureValidator#recoverSigner: INVALID_SIGNER" + ); + + return signer; + } +} diff --git a/test/BaseUSDbCTest.t.sol b/test/BaseUSDbCTest.t.sol index 4996e79..1abffee 100644 --- a/test/BaseUSDbCTest.t.sol +++ b/test/BaseUSDbCTest.t.sol @@ -3,12 +3,13 @@ pragma solidity 0.8.21; import { Test } from "forge-std/Test.sol"; import { CometWrapper, CometInterface, ICometRewards, CometHelpers, ERC20 } from "../src/CometWrapper.sol"; +import { BySigTest } from "./BySig.t.sol"; import { CometWrapperTest } from "./CometWrapper.t.sol"; import { CometWrapperInvariantTest } from "./CometWrapperInvariant.t.sol"; import { EncumberTest } from "./Encumber.t.sol"; import { RewardsTest } from "./Rewards.t.sol"; -contract BaseUSDbCTest is CometWrapperTest, CometWrapperInvariantTest, EncumberTest, RewardsTest { +contract BaseUSDbCTest is CometWrapperTest, CometWrapperInvariantTest, EncumberTest, RewardsTest, BySigTest { string public override NETWORK = "base"; uint256 public override FORK_BLOCK_NUMBER = 4791144; diff --git a/test/BySig.t.sol b/test/BySig.t.sol new file mode 100644 index 0000000..173ed9d --- /dev/null +++ b/test/BySig.t.sol @@ -0,0 +1,1140 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import { CoreTest, CometHelpers, CometWrapper, ERC20, ICometRewards } from "./CoreTest.sol"; + +// Tests for `permit` and `encumberBySig` +abstract contract BySigTest is CoreTest { + bytes32 internal constant AUTHORIZATION_TYPEHASH = keccak256("Authorization(address owner,address spender,uint256 amount,uint256 nonce,uint256 expiry)"); + bytes32 internal constant ENCUMBER_TYPEHASH = keccak256("Encumber(address owner,address taker,uint256 amount,uint256 nonce,uint256 expiry)"); + + function aliceAuthorization(uint256 amount, uint256 nonce, uint256 expiry) internal view returns (uint8, bytes32, bytes32) { + bytes32 structHash = keccak256(abi.encode(AUTHORIZATION_TYPEHASH, alice, bob, amount, nonce, expiry)); + bytes32 digest = keccak256(abi.encodePacked("\x19\x01", cometWrapper.DOMAIN_SEPARATOR(), structHash)); + return vm.sign(alicePrivateKey, digest); + } + + function aliceContractAuthorization(uint256 amount, uint256 nonce, uint256 expiry) internal view returns (uint8, bytes32, bytes32) { + bytes32 structHash = keccak256(abi.encode(AUTHORIZATION_TYPEHASH, aliceContract, bob, amount, nonce, expiry)); + bytes32 digest = keccak256(abi.encodePacked("\x19\x01", cometWrapper.DOMAIN_SEPARATOR(), structHash)); + return vm.sign(alicePrivateKey, digest); + } + + function aliceEncumberAuthorization(uint256 amount, uint256 nonce, uint256 expiry) internal view returns (uint8, bytes32, bytes32) { + bytes32 structHash = keccak256(abi.encode(ENCUMBER_TYPEHASH, alice, bob, amount, nonce, expiry)); + bytes32 digest = keccak256(abi.encodePacked("\x19\x01", cometWrapper.DOMAIN_SEPARATOR(), structHash)); + return vm.sign(alicePrivateKey, digest); + } + + function aliceContractEncumberAuthorization(uint256 amount, uint256 nonce, uint256 expiry) internal view returns (uint8, bytes32, bytes32) { + bytes32 structHash = keccak256(abi.encode(ENCUMBER_TYPEHASH, aliceContract, bob, amount, nonce, expiry)); + bytes32 digest = keccak256(abi.encodePacked("\x19\x01", cometWrapper.DOMAIN_SEPARATOR(), structHash)); + return vm.sign(alicePrivateKey, digest); + } + + /* ===== Permit ===== */ + + function test_permit() public { + // bob's allowance from alice is 0 + assertEq(cometWrapper.allowance(alice, bob), 0); + + uint256 allowance = 123e18; + uint256 nonce = cometWrapper.nonces(alice); + uint256 expiry = block.timestamp + 1000; + + (uint8 v, bytes32 r, bytes32 s) = aliceAuthorization(allowance, nonce, expiry); + + // bob calls permit with the signature + vm.prank(bob); + cometWrapper.permit(alice, bob, allowance, expiry, v, r, s); + + // bob's allowance from alice equals allowance + assertEq(cometWrapper.allowance(alice, bob), allowance); + + // alice's nonce is incremented + assertEq(cometWrapper.nonces(alice), nonce + 1); + } + + function test_permit_revertsForBadOwner() public { + // bob's allowance from alice is 0 + assertEq(cometWrapper.allowance(alice, bob), 0); + + uint256 allowance = 123e18; + uint256 nonce = cometWrapper.nonces(alice); + uint256 expiry = block.timestamp + 1000; + + (uint8 v, bytes32 r, bytes32 s) = aliceAuthorization(allowance, nonce, expiry); + + // bob calls permit with the signature, but he manipulates the owner + vm.prank(bob); + vm.expectRevert(CometWrapper.BadSignatory.selector); + cometWrapper.permit(charlie, bob, allowance, expiry, v, r, s); + + // bob's allowance from alice is unchanged + assertEq(cometWrapper.allowance(alice, bob), 0); + + // alice's nonce is not incremented + assertEq(cometWrapper.nonces(alice), nonce); + } + + function test_permit_revertsForBadSpender() public { + // bob's allowance from alice is 0 + assertEq(cometWrapper.allowance(alice, bob), 0); + + uint256 allowance = 123e18; + uint256 nonce = cometWrapper.nonces(alice); + uint256 expiry = block.timestamp + 1000; + + (uint8 v, bytes32 r, bytes32 s) = aliceAuthorization(allowance, nonce, expiry); + + // bob calls permit with the signature, but he manipulates the spender + vm.prank(bob); + vm.expectRevert(CometWrapper.BadSignatory.selector); + cometWrapper.permit(alice, charlie, allowance, expiry, v, r, s); + + // bob's allowance from alice is unchanged + assertEq(cometWrapper.allowance(alice, bob), 0); + + // alice's nonce is not incremented + assertEq(cometWrapper.nonces(alice), nonce); + } + + function test_permit_revertsForBadAmount() public { + // bob's allowance from alice is 0 + assertEq(cometWrapper.allowance(alice, bob), 0); + + uint256 allowance = 123e18; + uint256 nonce = cometWrapper.nonces(alice); + uint256 expiry = block.timestamp + 1000; + + (uint8 v, bytes32 r, bytes32 s) = aliceAuthorization(allowance, nonce, expiry); + + // bob calls permit with the signature, but he manipulates the allowance + vm.prank(bob); + vm.expectRevert(CometWrapper.BadSignatory.selector); + cometWrapper.permit(alice, bob, allowance + 1 wei, expiry, v, r, s); + + // bob's allowance from alice is unchanged + assertEq(cometWrapper.allowance(alice, bob), 0); + + // alice's nonce is not incremented + assertEq(cometWrapper.nonces(alice), nonce); + } + + function test_permit_revertsForBadExpiry() public { + // bob's allowance from alice is 0 + assertEq(cometWrapper.allowance(alice, bob), 0); + + uint256 allowance = 123e18; + uint256 nonce = cometWrapper.nonces(alice); + uint256 expiry = block.timestamp + 1000; + + (uint8 v, bytes32 r, bytes32 s) = aliceAuthorization(allowance, nonce, expiry); + + // bob calls permit with the signature, but he manipulates the expiry + vm.prank(bob); + vm.expectRevert(CometWrapper.BadSignatory.selector); + cometWrapper.permit(alice, bob, allowance, expiry + 1, v, r, s); + + // bob's allowance from alice is unchanged + assertEq(cometWrapper.allowance(alice, bob), 0); + + // alice's nonce is not incremented + assertEq(cometWrapper.nonces(alice), nonce); + } + + function test_permit_revertsForBadNonce() public { + // bob's allowance from alice is 0 + assertEq(cometWrapper.allowance(alice, bob), 0); + + // alice signs an authorization with an invalid nonce + uint256 allowance = 123e18; + uint256 nonce = cometWrapper.nonces(alice); + uint256 badNonce = nonce + 1; + uint256 expiry = block.timestamp + 1000; + + (uint8 v, bytes32 r, bytes32 s) = aliceAuthorization(allowance, badNonce, expiry); + + // bob calls permit with the signature with an invalid nonce + vm.prank(bob); + vm.expectRevert(CometWrapper.BadSignatory.selector); + cometWrapper.permit(alice, bob, allowance, expiry, v, r, s); + + // bob's allowance from alice is unchanged + assertEq(cometWrapper.allowance(alice, bob), 0); + + // alice's nonce is not incremented + assertEq(cometWrapper.nonces(alice), nonce); + } + + function test_permit_revertsOnRepeatedCall() public { + // bob's allowance from alice is 0 + assertEq(cometWrapper.allowance(alice, bob), 0); + + uint256 allowance = 123e18; + uint256 nonce = cometWrapper.nonces(alice); + uint256 expiry = block.timestamp + 1000; + + (uint8 v, bytes32 r, bytes32 s) = aliceAuthorization(allowance, nonce, expiry); + + // bob calls permit with the signature + vm.prank(bob); + cometWrapper.permit(alice, bob, allowance, expiry, v, r, s); + + // bob's allowance from alice equals allowance + assertEq(cometWrapper.allowance(alice, bob), allowance); + + // alice's nonce is incremented + assertEq(cometWrapper.nonces(alice), nonce + 1); + + // alice revokes bob's allowance + vm.prank(alice); + cometWrapper.approve(bob, 0); + assertEq(cometWrapper.allowance(alice, bob), 0); + + // bob tries to reuse the same signature twice + vm.prank(bob); + vm.expectRevert(CometWrapper.BadSignatory.selector); + cometWrapper.permit(alice, bob, allowance, expiry, v, r, s); + + // bob's allowance from alice is unchanged + assertEq(cometWrapper.allowance(alice, bob), 0); + + // alice's nonce is not incremented + assertEq(cometWrapper.nonces(alice), nonce + 1); + } + + function test_permit_revertsForExpiredSignature() public { + // bob's allowance from alice is 0 + assertEq(cometWrapper.allowance(alice, bob), 0); + + uint256 allowance = 123e18; + uint256 nonce = cometWrapper.nonces(alice); + uint256 expiry = block.timestamp + 1000; + + (uint8 v, bytes32 r, bytes32 s) = aliceAuthorization(allowance, nonce, expiry); + + // the expiry block arrives + vm.warp(expiry); + + // bob calls permit with the signature after the expiry + vm.prank(bob); + vm.expectRevert(CometWrapper.SignatureExpired.selector); + cometWrapper.permit(alice, bob, allowance, expiry, v, r, s); + + // bob's allowance from alice is unchanged + assertEq(cometWrapper.allowance(alice, bob), 0); + + // alice's nonce is not incremented + assertEq(cometWrapper.nonces(alice), nonce); + } + + function test_permit_revertsInvalidS() public { + // bob's allowance from alice is 0 + assertEq(cometWrapper.allowance(alice, bob), 0); + + uint256 allowance = 123e18; + uint256 nonce = cometWrapper.nonces(alice); + uint256 expiry = block.timestamp + 1000; + + (uint8 v, bytes32 r, ) = aliceAuthorization(allowance, nonce, expiry); + + // 1 greater than the max value of s + bytes32 invalidS = 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A1; + + // bob calls permit with the signature with invalid `s` value + vm.prank(bob); + vm.expectRevert(CometWrapper.InvalidSignatureS.selector); + cometWrapper.permit(alice, bob, allowance, expiry, v, r, invalidS); + + // bob's allowance from alice is unchanged + assertEq(cometWrapper.allowance(alice, bob), 0); + + // alice's nonce is not incremented + assertEq(cometWrapper.nonces(alice), nonce); + } + + /* ===== EncumberBySig ===== */ + + function test_encumberBySig() public { + uint256 aliceBalance = 100e18; + uint256 encumbranceAmount = 60e18; + + // alice has 100 wrapped tokens + deal(address(cometWrapper), alice, aliceBalance); + + assertEq(cometWrapper.balanceOf(alice), aliceBalance); + assertEq(cometWrapper.availableBalanceOf(alice), aliceBalance); + assertEq(cometWrapper.encumberedBalanceOf(alice), 0); + assertEq(cometWrapper.encumbrances(alice, bob), 0); + + uint256 nonce = cometWrapper.nonces(alice); + uint256 expiry = block.timestamp + 1000; + + (uint8 v, bytes32 r, bytes32 s) = aliceEncumberAuthorization(encumbranceAmount, nonce, expiry); + + // bob calls encumberBySig with the signature + vm.prank(bob); + cometWrapper.encumberBySig(alice, bob, encumbranceAmount, expiry, v, r, s); + + assertEq(cometWrapper.balanceOf(alice), aliceBalance); + assertEq(cometWrapper.availableBalanceOf(alice), aliceBalance - encumbranceAmount); + assertEq(cometWrapper.encumberedBalanceOf(alice), encumbranceAmount); + assertEq(cometWrapper.encumbrances(alice, bob), encumbranceAmount); + + // alice's nonce is incremented + assertEq(cometWrapper.nonces(alice), nonce + 1); + } + + function test_encumberBySig_revertsForBadOwner() public { + uint256 aliceBalance = 100e18; + uint256 encumbranceAmount = 60e18; + + // alice has 100 wrapped tokens + deal(address(cometWrapper), alice, aliceBalance); + + assertEq(cometWrapper.balanceOf(alice), aliceBalance); + assertEq(cometWrapper.availableBalanceOf(alice), aliceBalance); + assertEq(cometWrapper.encumberedBalanceOf(alice), 0); + assertEq(cometWrapper.encumbrances(alice, bob), 0); + + uint256 nonce = cometWrapper.nonces(alice); + uint256 expiry = block.timestamp + 1000; + + (uint8 v, bytes32 r, bytes32 s) = aliceEncumberAuthorization(encumbranceAmount, nonce, expiry); + + // bob calls encumberBySig with the signature, but he manipulates the owner + vm.prank(bob); + vm.expectRevert(CometWrapper.BadSignatory.selector); + cometWrapper.encumberBySig(charlie, bob, encumbranceAmount, expiry, v, r, s); + + // no encumbrance is created + assertEq(cometWrapper.balanceOf(alice), aliceBalance); + assertEq(cometWrapper.availableBalanceOf(alice), aliceBalance); + assertEq(cometWrapper.encumberedBalanceOf(alice), 0); + assertEq(cometWrapper.encumbrances(alice, bob), 0); + + // alice's nonce is not incremented + assertEq(cometWrapper.nonces(alice), nonce); + } + + function test_encumberBySig_revertsForBadSpender() public { + uint256 aliceBalance = 100e18; + uint256 encumbranceAmount = 60e18; + + // alice has 100 wrapped tokens + deal(address(cometWrapper), alice, aliceBalance); + + assertEq(cometWrapper.balanceOf(alice), aliceBalance); + assertEq(cometWrapper.availableBalanceOf(alice), aliceBalance); + assertEq(cometWrapper.encumberedBalanceOf(alice), 0); + assertEq(cometWrapper.encumbrances(alice, bob), 0); + + uint256 nonce = cometWrapper.nonces(alice); + uint256 expiry = block.timestamp + 1000; + + (uint8 v, bytes32 r, bytes32 s) = aliceEncumberAuthorization(encumbranceAmount, nonce, expiry); + + // bob calls encumberBySig with the signature, but he manipulates the spender + vm.prank(bob); + vm.expectRevert(CometWrapper.BadSignatory.selector); + cometWrapper.encumberBySig(alice, charlie, encumbranceAmount, expiry, v, r, s); + + // no encumbrance is created + assertEq(cometWrapper.balanceOf(alice), aliceBalance); + assertEq(cometWrapper.availableBalanceOf(alice), aliceBalance); + assertEq(cometWrapper.encumberedBalanceOf(alice), 0); + assertEq(cometWrapper.encumbrances(alice, bob), 0); + + // alice's nonce is not incremented + assertEq(cometWrapper.nonces(alice), nonce); + } + + function test_encumberBySig_revertsForBadAmount() public { + uint256 aliceBalance = 100e18; + uint256 encumbranceAmount = 60e18; + + // alice has 100 wrapped tokens + deal(address(cometWrapper), alice, aliceBalance); + + assertEq(cometWrapper.balanceOf(alice), aliceBalance); + assertEq(cometWrapper.availableBalanceOf(alice), aliceBalance); + assertEq(cometWrapper.encumberedBalanceOf(alice), 0); + assertEq(cometWrapper.encumbrances(alice, bob), 0); + + uint256 nonce = cometWrapper.nonces(alice); + uint256 expiry = block.timestamp + 1000; + + (uint8 v, bytes32 r, bytes32 s) = aliceEncumberAuthorization(encumbranceAmount, nonce, expiry); + + // bob calls encumberBySig with the signature, but he manipulates the encumbranceAmount + vm.prank(bob); + vm.expectRevert(CometWrapper.BadSignatory.selector); + cometWrapper.encumberBySig(alice, bob, encumbranceAmount + 1 wei, expiry, v, r, s); + + // no encumbrance is created + assertEq(cometWrapper.balanceOf(alice), aliceBalance); + assertEq(cometWrapper.availableBalanceOf(alice), aliceBalance); + assertEq(cometWrapper.encumberedBalanceOf(alice), 0); + assertEq(cometWrapper.encumbrances(alice, bob), 0); + + // alice's nonce is not incremented + assertEq(cometWrapper.nonces(alice), nonce); + } + + function test_encumberBySig_revertsForBadExpiry() public { + uint256 aliceBalance = 100e18; + uint256 encumbranceAmount = 60e18; + + // alice has 100 wrapped tokens + deal(address(cometWrapper), alice, aliceBalance); + + assertEq(cometWrapper.balanceOf(alice), aliceBalance); + assertEq(cometWrapper.availableBalanceOf(alice), aliceBalance); + assertEq(cometWrapper.encumberedBalanceOf(alice), 0); + assertEq(cometWrapper.encumbrances(alice, bob), 0); + + uint256 nonce = cometWrapper.nonces(alice); + uint256 expiry = block.timestamp + 1000; + + (uint8 v, bytes32 r, bytes32 s) = aliceEncumberAuthorization(encumbranceAmount, nonce, expiry); + + // bob calls encumberBySig with the signature, but he manipulates the expiry + vm.prank(bob); + vm.expectRevert(CometWrapper.BadSignatory.selector); + cometWrapper.encumberBySig(alice, bob, encumbranceAmount, expiry + 1, v, r, s); + + // no encumbrance is created + assertEq(cometWrapper.balanceOf(alice), aliceBalance); + assertEq(cometWrapper.availableBalanceOf(alice), aliceBalance); + assertEq(cometWrapper.encumberedBalanceOf(alice), 0); + assertEq(cometWrapper.encumbrances(alice, bob), 0); + + // alice's nonce is not incremented + assertEq(cometWrapper.nonces(alice), nonce); + } + + function test_encumberBySig_revertsForBadNonce() public { + uint256 aliceBalance = 100e18; + uint256 encumbranceAmount = 60e18; + + // alice has 100 wrapped tokens + deal(address(cometWrapper), alice, aliceBalance); + + assertEq(cometWrapper.balanceOf(alice), aliceBalance); + assertEq(cometWrapper.availableBalanceOf(alice), aliceBalance); + assertEq(cometWrapper.encumberedBalanceOf(alice), 0); + assertEq(cometWrapper.encumbrances(alice, bob), 0); + + // alice signs an authorization with an invalid nonce + uint256 nonce = cometWrapper.nonces(alice); + uint256 badNonce = nonce + 1; + uint256 expiry = block.timestamp + 1000; + + (uint8 v, bytes32 r, bytes32 s) = aliceEncumberAuthorization(encumbranceAmount, badNonce, expiry); + + // bob calls encumberBySig with the signature with an invalid nonce + vm.prank(bob); + vm.expectRevert(CometWrapper.BadSignatory.selector); + cometWrapper.encumberBySig(alice, bob, encumbranceAmount, expiry, v, r, s); + + // no encumbrance is created + assertEq(cometWrapper.balanceOf(alice), aliceBalance); + assertEq(cometWrapper.availableBalanceOf(alice), aliceBalance); + assertEq(cometWrapper.encumberedBalanceOf(alice), 0); + assertEq(cometWrapper.encumbrances(alice, bob), 0); + + // alice's nonce is not incremented + assertEq(cometWrapper.nonces(alice), nonce); + } + + function test_encumberBySig_revertsOnRepeatedCall() public { + uint256 aliceBalance = 100e18; + uint256 encumbranceAmount = 60e18; + uint256 transferAmount = 30e18; + + // alice has 100 wrapped tokens + deal(address(cometWrapper), alice, aliceBalance); + + assertEq(cometWrapper.balanceOf(alice), aliceBalance); + assertEq(cometWrapper.availableBalanceOf(alice), aliceBalance); + assertEq(cometWrapper.encumberedBalanceOf(alice), 0); + assertEq(cometWrapper.encumbrances(alice, bob), 0); + + uint256 nonce = cometWrapper.nonces(alice); + uint256 expiry = block.timestamp + 1000; + + (uint8 v, bytes32 r, bytes32 s) = aliceEncumberAuthorization(encumbranceAmount, nonce, expiry); + + // bob calls encumberBySig with the signature + vm.startPrank(bob); + cometWrapper.encumberBySig(alice, bob, encumbranceAmount, expiry, v, r, s); + + // the encumbrance is created + assertEq(cometWrapper.balanceOf(alice), aliceBalance); + assertEq(cometWrapper.availableBalanceOf(alice), aliceBalance - encumbranceAmount); + assertEq(cometWrapper.encumberedBalanceOf(alice), encumbranceAmount); + assertEq(cometWrapper.encumbrances(alice, bob), encumbranceAmount); + + // alice's nonce is incremented + assertEq(cometWrapper.nonces(alice), nonce + 1); + + // bob uses some of the encumbrance to transfer to himself + cometWrapper.transferFrom(alice, bob, transferAmount); + + assertEq(cometWrapper.balanceOf(alice), aliceBalance - transferAmount); + assertEq(cometWrapper.availableBalanceOf(alice), aliceBalance - encumbranceAmount); + assertEq(cometWrapper.encumberedBalanceOf(alice), encumbranceAmount - transferAmount); + assertEq(cometWrapper.encumbrances(alice, bob), encumbranceAmount - transferAmount); + + // bob tries to reuse the same signature twice + vm.expectRevert(CometWrapper.BadSignatory.selector); + cometWrapper.encumberBySig(alice, bob, encumbranceAmount, expiry, v, r, s); + + // no new encumbrance is created + assertEq(cometWrapper.balanceOf(alice), aliceBalance - transferAmount); + assertEq(cometWrapper.availableBalanceOf(alice), aliceBalance - encumbranceAmount); + assertEq(cometWrapper.encumberedBalanceOf(alice), encumbranceAmount - transferAmount); + assertEq(cometWrapper.encumbrances(alice, bob), encumbranceAmount - transferAmount); + + // alice's nonce is not incremented a second time + assertEq(cometWrapper.nonces(alice), nonce + 1); + + vm.stopPrank(); + } + + function test_encumberBySig_revertsForExpiredSignature() public { + uint256 aliceBalance = 100e18; + uint256 encumbranceAmount = 60e18; + + // alice has 100 wrapped tokens + deal(address(cometWrapper), alice, aliceBalance); + + assertEq(cometWrapper.balanceOf(alice), aliceBalance); + assertEq(cometWrapper.availableBalanceOf(alice), aliceBalance); + assertEq(cometWrapper.encumberedBalanceOf(alice), 0); + assertEq(cometWrapper.encumbrances(alice, bob), 0); + + uint256 nonce = cometWrapper.nonces(alice); + // Fix for via-IR issue: https://github.com/foundry-rs/foundry/issues/3312#issuecomment-1255264273 + uint256 expiry = uint248(block.timestamp + 1000); + + (uint8 v, bytes32 r, bytes32 s) = aliceEncumberAuthorization(encumbranceAmount, nonce, expiry); + + // the expiry block arrives + vm.warp(expiry); + + // bob calls encumberBySig with the signature after the expiry + vm.prank(bob); + vm.expectRevert(CometWrapper.SignatureExpired.selector); + cometWrapper.encumberBySig(alice, bob, encumbranceAmount, expiry, v, r, s); + + // no encumbrance is created + assertEq(cometWrapper.balanceOf(alice), aliceBalance); + assertEq(cometWrapper.availableBalanceOf(alice), aliceBalance); + assertEq(cometWrapper.encumberedBalanceOf(alice), 0); + assertEq(cometWrapper.encumbrances(alice, bob), 0); + + // alice's nonce is not incremented + assertEq(cometWrapper.nonces(alice), nonce); + } + + function test_encumberBySig_revertsInvalidS() public { + uint256 aliceBalance = 100e18; + uint256 encumbranceAmount = 60e18; + + // alice has 100 wrapped tokens + deal(address(cometWrapper), alice, aliceBalance); + + assertEq(cometWrapper.balanceOf(alice), aliceBalance); + assertEq(cometWrapper.availableBalanceOf(alice), aliceBalance); + assertEq(cometWrapper.encumberedBalanceOf(alice), 0); + assertEq(cometWrapper.encumbrances(alice, bob), 0); + + uint256 nonce = cometWrapper.nonces(alice); + uint256 expiry = block.timestamp + 1000; + + (uint8 v, bytes32 r, ) = aliceEncumberAuthorization(encumbranceAmount, nonce, expiry); + + // 1 greater than the max value of s + bytes32 invalidS = 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A1; + + // bob calls encumberBySig with the signature, but he manipulates the expiry + vm.prank(bob); + vm.expectRevert(CometWrapper.InvalidSignatureS.selector); + cometWrapper.encumberBySig(alice, bob, encumbranceAmount, expiry, v, r, invalidS); + + // no encumbrance is created + assertEq(cometWrapper.balanceOf(alice), aliceBalance); + assertEq(cometWrapper.availableBalanceOf(alice), aliceBalance); + assertEq(cometWrapper.encumberedBalanceOf(alice), 0); + assertEq(cometWrapper.encumbrances(alice, bob), 0); + + // alice's nonce is not incremented + assertEq(cometWrapper.nonces(alice), nonce); + } + + /* ===== EIP1271 Tests ===== */ + + function test_permitEIP1271() public { + // bob's allowance from alice's contract is 0 + assertEq(cometWrapper.allowance(aliceContract, bob), 0); + + uint256 allowance = 123e18; + uint256 nonce = cometWrapper.nonces(aliceContract); + uint256 expiry = block.timestamp + 1000; + + (uint8 v, bytes32 r, bytes32 s) = aliceContractAuthorization(allowance, nonce, expiry); + + // bob calls permit with the signature + vm.prank(bob); + cometWrapper.permit(aliceContract, bob, allowance, expiry, v, r, s); + + // bob's allowance from alice's contract equals allowance + assertEq(cometWrapper.allowance(aliceContract, bob), allowance); + + // alice's contract's nonce is incremented + assertEq(cometWrapper.nonces(aliceContract), nonce + 1); + } + + function test_permit_revertsForBadOwnerEIP1271() public { + // bob's allowance from alice's contract is 0 + assertEq(cometWrapper.allowance(aliceContract, bob), 0); + + uint256 allowance = 123e18; + uint256 nonce = cometWrapper.nonces(aliceContract); + uint256 expiry = block.timestamp + 1000; + + (uint8 v, bytes32 r, bytes32 s) = aliceContractAuthorization(allowance, nonce, expiry); + + // bob calls permit with the signature, but he manipulates the owner + vm.prank(bob); + vm.expectRevert(CometWrapper.BadSignatory.selector); + cometWrapper.permit(charlie, bob, allowance, expiry, v, r, s); + + // bob's allowance from alice's contract is unchanged + assertEq(cometWrapper.allowance(aliceContract, bob), 0); + + // alice's contract's nonce is not incremented + assertEq(cometWrapper.nonces(aliceContract), nonce); + } + + function test_permit_revertsForBadSpenderEIP1271() public { + // bob's allowance from alice's contract is 0 + assertEq(cometWrapper.allowance(aliceContract, bob), 0); + + uint256 allowance = 123e18; + uint256 nonce = cometWrapper.nonces(aliceContract); + uint256 expiry = block.timestamp + 1000; + + (uint8 v, bytes32 r, bytes32 s) = aliceContractAuthorization(allowance, nonce, expiry); + + // bob calls permit with the signature, but he manipulates the spender + vm.prank(bob); + vm.expectRevert(CometWrapper.BadSignatory.selector); + cometWrapper.permit(aliceContract, charlie, allowance, expiry, v, r, s); + + // bob's allowance from alice's contract is unchanged + assertEq(cometWrapper.allowance(aliceContract, bob), 0); + + // alice's contract's nonce is not incremented + assertEq(cometWrapper.nonces(aliceContract), nonce); + } + + function test_permit_revertsForBadAmountEIP1271() public { + // bob's allowance from alice's contract is 0 + assertEq(cometWrapper.allowance(aliceContract, bob), 0); + + uint256 allowance = 123e18; + uint256 nonce = cometWrapper.nonces(aliceContract); + uint256 expiry = block.timestamp + 1000; + + (uint8 v, bytes32 r, bytes32 s) = aliceContractAuthorization(allowance, nonce, expiry); + + // bob calls permit with the signature, but he manipulates the allowance + vm.prank(bob); + vm.expectRevert(CometWrapper.BadSignatory.selector); + cometWrapper.permit(aliceContract, bob, allowance + 1 wei, expiry, v, r, s); + + // bob's allowance from alice's contract is unchanged + assertEq(cometWrapper.allowance(aliceContract, bob), 0); + + // alice's contract's nonce is not incremented + assertEq(cometWrapper.nonces(aliceContract), nonce); + } + + function test_permit_revertsForBadExpiryEIP1271() public { + // bob's allowance from alice's contract is 0 + assertEq(cometWrapper.allowance(aliceContract, bob), 0); + + uint256 allowance = 123e18; + uint256 nonce = cometWrapper.nonces(aliceContract); + uint256 expiry = block.timestamp + 1000; + + (uint8 v, bytes32 r, bytes32 s) = aliceContractAuthorization(allowance, nonce, expiry); + + // bob calls permit with the signature, but he manipulates the expiry + vm.prank(bob); + vm.expectRevert(CometWrapper.BadSignatory.selector); + cometWrapper.permit(aliceContract, bob, allowance, expiry + 1, v, r, s); + + // bob's allowance from alice's contract is unchanged + assertEq(cometWrapper.allowance(aliceContract, bob), 0); + + // alice's contract's nonce is not incremented + assertEq(cometWrapper.nonces(alice), nonce); + } + + function test_permit_revertsForBadNonceEIP1271() public { + // bob's allowance from alice's contract is 0 + assertEq(cometWrapper.allowance(aliceContract, bob), 0); + + // alice signs an authorization with an invalid nonce + uint256 allowance = 123e18; + uint256 nonce = cometWrapper.nonces(aliceContract); + uint256 badNonce = nonce + 1; + uint256 expiry = block.timestamp + 1000; + + (uint8 v, bytes32 r, bytes32 s) = aliceContractAuthorization(allowance, badNonce, expiry); + + // bob calls permit with the signature with an invalid nonce + vm.prank(bob); + vm.expectRevert(CometWrapper.BadSignatory.selector); + cometWrapper.permit(aliceContract, bob, allowance, expiry, v, r, s); + + // bob's allowance from alice's contract is unchanged + assertEq(cometWrapper.allowance(aliceContract, bob), 0); + + // alice's contract's nonce is not incremented + assertEq(cometWrapper.nonces(aliceContract), nonce); + } + + function test_permit_revertsOnRepeatedCallEIP1271() public { + // bob's allowance from alice's contract is 0 + assertEq(cometWrapper.allowance(aliceContract, bob), 0); + + uint256 allowance = 123e18; + uint256 nonce = cometWrapper.nonces(aliceContract); + uint256 expiry = block.timestamp + 1000; + + (uint8 v, bytes32 r, bytes32 s) = aliceContractAuthorization(allowance, nonce, expiry); + + // bob calls permit with the signature + vm.prank(bob); + cometWrapper.permit(aliceContract, bob, allowance, expiry, v, r, s); + + // bob's allowance from alice's contract equals allowance + assertEq(cometWrapper.allowance(aliceContract, bob), allowance); + + // alice's contract's nonce is incremented + assertEq(cometWrapper.nonces(aliceContract), nonce + 1); + + // alice revokes bob's allowance + vm.prank(aliceContract); + cometWrapper.approve(bob, 0); + assertEq(cometWrapper.allowance(aliceContract, bob), 0); + + // bob tries to reuse the same signature twice + vm.prank(bob); + vm.expectRevert(CometWrapper.BadSignatory.selector); + cometWrapper.permit(aliceContract, bob, allowance, expiry, v, r, s); + + // bob's allowance from alice's contract is unchanged + assertEq(cometWrapper.allowance(aliceContract, bob), 0); + + // alice's contract's nonce is not incremented + assertEq(cometWrapper.nonces(aliceContract), nonce + 1); + } + + function test_permit_revertsForExpiredSignatureEIP1271() public { + // bob's allowance from alice's contract is 0 + assertEq(cometWrapper.allowance(aliceContract, bob), 0); + + uint256 allowance = 123e18; + uint256 nonce = cometWrapper.nonces(aliceContract); + uint256 expiry = block.timestamp + 1000; + + (uint8 v, bytes32 r, bytes32 s) = aliceContractAuthorization(allowance, nonce, expiry); + + // the expiry block arrives + vm.warp(expiry); + + // bob calls permit with the signature after the expiry + vm.prank(bob); + vm.expectRevert(CometWrapper.SignatureExpired.selector); + cometWrapper.permit(aliceContract, bob, allowance, expiry, v, r, s); + + // bob's allowance from alice's contract is unchanged + assertEq(cometWrapper.allowance(aliceContract, bob), 0); + + // alice's contract's nonce is not incremented + assertEq(cometWrapper.nonces(aliceContract), nonce); + } + + function test_permit_revertsInvalidVEIP1271() public { + // bob's allowance from alice's contract is 0 + assertEq(cometWrapper.allowance(aliceContract, bob), 0); + + uint256 allowance = 123e18; + uint256 nonce = cometWrapper.nonces(aliceContract); + uint256 expiry = block.timestamp + 1000; + + (, bytes32 r, bytes32 s) = aliceContractAuthorization(allowance, nonce, expiry); + uint8 invalidV = 26; + + // bob calls permit with the signature with invalid `v` value + vm.prank(bob); + vm.expectRevert(CometWrapper.EIP1271VerificationFailed.selector); + cometWrapper.permit(aliceContract, bob, allowance, expiry, invalidV, r, s); + + // bob's allowance from alice's contract is unchanged + assertEq(cometWrapper.allowance(aliceContract, bob), 0); + + // alice's contract's nonce is not incremented + assertEq(cometWrapper.nonces(aliceContract), nonce); + } + + function test_permit_revertsInvalidSEIP1271() public { + // bob's allowance from alice's contract is 0 + assertEq(cometWrapper.allowance(aliceContract, bob), 0); + + uint256 allowance = 123e18; + uint256 nonce = cometWrapper.nonces(aliceContract); + uint256 expiry = block.timestamp + 1000; + + (uint8 v, bytes32 r, ) = aliceContractAuthorization(allowance, nonce, expiry); + + // 1 greater than the max value of s + bytes32 invalidS = 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A1; + + // bob calls permit with the signature with invalid `s` value + vm.prank(bob); + vm.expectRevert(CometWrapper.EIP1271VerificationFailed.selector); + cometWrapper.permit(aliceContract, bob, allowance, expiry, v, r, invalidS); + + // bob's allowance from alice's contract is unchanged + assertEq(cometWrapper.allowance(aliceContract, bob), 0); + + // alice's contract's nonce is not incremented + assertEq(cometWrapper.nonces(aliceContract), nonce); + } + + function test_encumberBySigEIP1271() public { + uint256 aliceBalance = 100e18; + uint256 encumbranceAmount = 60e18; + + // alice's contract has 100 wrapped tokens + deal(address(cometWrapper), aliceContract, aliceBalance); + + assertEq(cometWrapper.balanceOf(aliceContract), aliceBalance); + assertEq(cometWrapper.availableBalanceOf(aliceContract), aliceBalance); + assertEq(cometWrapper.encumberedBalanceOf(aliceContract), 0); + assertEq(cometWrapper.encumbrances(aliceContract, bob), 0); + + uint256 nonce = cometWrapper.nonces(aliceContract); + uint256 expiry = block.timestamp + 1000; + + (uint8 v, bytes32 r, bytes32 s) = aliceContractEncumberAuthorization(encumbranceAmount, nonce, expiry); + + // bob calls encumberBySig with the signature + vm.prank(bob); + cometWrapper.encumberBySig(aliceContract, bob, encumbranceAmount, expiry, v, r, s); + + assertEq(cometWrapper.balanceOf(aliceContract), aliceBalance); + assertEq(cometWrapper.availableBalanceOf(aliceContract), aliceBalance - encumbranceAmount); + assertEq(cometWrapper.encumberedBalanceOf(aliceContract), encumbranceAmount); + assertEq(cometWrapper.encumbrances(aliceContract, bob), encumbranceAmount); + + // alice's contract's nonce is incremented + assertEq(cometWrapper.nonces(aliceContract), nonce + 1); + } + + function test_encumberBySig_revertsForBadSpenderEIP1271() public { + uint256 aliceBalance = 100e18; + uint256 encumbranceAmount = 60e18; + + // alice's contract has 100 wrapped tokens + deal(address(cometWrapper), aliceContract, aliceBalance); + + assertEq(cometWrapper.balanceOf(aliceContract), aliceBalance); + assertEq(cometWrapper.availableBalanceOf(aliceContract), aliceBalance); + assertEq(cometWrapper.encumberedBalanceOf(aliceContract), 0); + assertEq(cometWrapper.encumbrances(aliceContract, bob), 0); + + uint256 nonce = cometWrapper.nonces(aliceContract); + uint256 expiry = block.timestamp + 1000; + + (uint8 v, bytes32 r, bytes32 s) = aliceContractEncumberAuthorization(encumbranceAmount, nonce, expiry); + + // bob calls encumberBySig with the signature, but he manipulates the spender + vm.prank(bob); + vm.expectRevert(CometWrapper.BadSignatory.selector); + cometWrapper.encumberBySig(aliceContract, charlie, encumbranceAmount, expiry, v, r, s); + + // no encumbrance is created + assertEq(cometWrapper.balanceOf(aliceContract), aliceBalance); + assertEq(cometWrapper.availableBalanceOf(aliceContract), aliceBalance); + assertEq(cometWrapper.encumberedBalanceOf(aliceContract), 0); + assertEq(cometWrapper.encumbrances(aliceContract, bob), 0); + + // alice's contract's nonce is not incremented + assertEq(cometWrapper.nonces(aliceContract), nonce); + } + + function test_encumberBySig_revertsForBadAmountEIP1271() public { + uint256 aliceBalance = 100e18; + uint256 encumbranceAmount = 60e18; + + // alice's contract has 100 wrapped tokens + deal(address(cometWrapper), aliceContract, aliceBalance); + + assertEq(cometWrapper.balanceOf(aliceContract), aliceBalance); + assertEq(cometWrapper.availableBalanceOf(aliceContract), aliceBalance); + assertEq(cometWrapper.encumberedBalanceOf(aliceContract), 0); + assertEq(cometWrapper.encumbrances(aliceContract, bob), 0); + + uint256 nonce = cometWrapper.nonces(aliceContract); + uint256 expiry = block.timestamp + 1000; + + (uint8 v, bytes32 r, bytes32 s) = aliceContractEncumberAuthorization(encumbranceAmount, nonce, expiry); + + // bob calls encumberBySig with the signature, but he manipulates the encumbranceAmount + vm.prank(bob); + vm.expectRevert(CometWrapper.BadSignatory.selector); + cometWrapper.encumberBySig(aliceContract, bob, encumbranceAmount + 1 wei, expiry, v, r, s); + + // no encumbrance is created + assertEq(cometWrapper.balanceOf(aliceContract), aliceBalance); + assertEq(cometWrapper.availableBalanceOf(aliceContract), aliceBalance); + assertEq(cometWrapper.encumberedBalanceOf(aliceContract), 0); + assertEq(cometWrapper.encumbrances(aliceContract, bob), 0); + + // alice's contract's nonce is not incremented + assertEq(cometWrapper.nonces(aliceContract), nonce); + } + + function test_encumberBySig_revertsForBadExpiryEIP1271() public { + uint256 aliceBalance = 100e18; + uint256 encumbranceAmount = 60e18; + + // alice's contract has 100 wrapped tokens + deal(address(cometWrapper), aliceContract, aliceBalance); + + assertEq(cometWrapper.balanceOf(aliceContract), aliceBalance); + assertEq(cometWrapper.availableBalanceOf(aliceContract), aliceBalance); + assertEq(cometWrapper.encumberedBalanceOf(aliceContract), 0); + assertEq(cometWrapper.encumbrances(aliceContract, bob), 0); + + uint256 nonce = cometWrapper.nonces(aliceContract); + uint256 expiry = block.timestamp + 1000; + + (uint8 v, bytes32 r, bytes32 s) = aliceContractEncumberAuthorization(encumbranceAmount, nonce, expiry); + + // bob calls encumberBySig with the signature, but he manipulates the expiry + vm.prank(bob); + vm.expectRevert(CometWrapper.BadSignatory.selector); + cometWrapper.encumberBySig(aliceContract, bob, encumbranceAmount, expiry + 1, v, r, s); + + // no encumbrance is created + assertEq(cometWrapper.balanceOf(aliceContract), aliceBalance); + assertEq(cometWrapper.availableBalanceOf(aliceContract), aliceBalance); + assertEq(cometWrapper.encumberedBalanceOf(aliceContract), 0); + assertEq(cometWrapper.encumbrances(aliceContract, bob), 0); + + // alice's contract's nonce is not incremented + assertEq(cometWrapper.nonces(aliceContract), nonce); + } + + function test_encumberBySig_revertsForBadNonceEIP1271() public { + uint256 aliceBalance = 100e18; + uint256 encumbranceAmount = 60e18; + + // alice's contract has 100 wrapped tokens + deal(address(cometWrapper), aliceContract, aliceBalance); + + assertEq(cometWrapper.balanceOf(aliceContract), aliceBalance); + assertEq(cometWrapper.availableBalanceOf(aliceContract), aliceBalance); + assertEq(cometWrapper.encumberedBalanceOf(aliceContract), 0); + assertEq(cometWrapper.encumbrances(aliceContract, bob), 0); + + // alice signs an authorization with an invalid nonce + uint256 nonce = cometWrapper.nonces(aliceContract); + uint256 badNonce = nonce + 1; + uint256 expiry = block.timestamp + 1000; + + (uint8 v, bytes32 r, bytes32 s) = aliceContractEncumberAuthorization(encumbranceAmount, badNonce, expiry); + + // bob calls encumberBySig with the signature with an invalid nonce + vm.prank(bob); + vm.expectRevert(CometWrapper.BadSignatory.selector); + cometWrapper.encumberBySig(aliceContract, bob, encumbranceAmount, expiry, v, r, s); + + // no encumbrance is created + assertEq(cometWrapper.balanceOf(aliceContract), aliceBalance); + assertEq(cometWrapper.availableBalanceOf(aliceContract), aliceBalance); + assertEq(cometWrapper.encumberedBalanceOf(aliceContract), 0); + assertEq(cometWrapper.encumbrances(aliceContract, bob), 0); + + // alice's contract's nonce is not incremented + assertEq(cometWrapper.nonces(aliceContract), nonce); + } + + function test_encumberBySig_revertsOnRepeatedCallEIP1271() public { + uint256 aliceBalance = 100e18; + uint256 encumbranceAmount = 60e18; + uint256 transferAmount = 30e18; + + // alice's contract has 100 wrapped tokens + deal(address(cometWrapper), aliceContract, aliceBalance); + + assertEq(cometWrapper.balanceOf(aliceContract), aliceBalance); + assertEq(cometWrapper.availableBalanceOf(aliceContract), aliceBalance); + assertEq(cometWrapper.encumberedBalanceOf(aliceContract), 0); + assertEq(cometWrapper.encumbrances(aliceContract, bob), 0); + + uint256 nonce = cometWrapper.nonces(alice); + uint256 expiry = block.timestamp + 1000; + + (uint8 v, bytes32 r, bytes32 s) = aliceContractEncumberAuthorization(encumbranceAmount, nonce, expiry); + + // bob calls encumberBySig with the signature + vm.startPrank(bob); + cometWrapper.encumberBySig(aliceContract, bob, encumbranceAmount, expiry, v, r, s); + + // the encumbrance is created + assertEq(cometWrapper.balanceOf(aliceContract), aliceBalance); + assertEq(cometWrapper.availableBalanceOf(aliceContract), aliceBalance - encumbranceAmount); + assertEq(cometWrapper.encumberedBalanceOf(aliceContract), encumbranceAmount); + assertEq(cometWrapper.encumbrances(aliceContract, bob), encumbranceAmount); + + // alice's contract's nonce is incremented + assertEq(cometWrapper.nonces(aliceContract), nonce + 1); + + // bob uses some of the encumbrance to transfer to himself + cometWrapper.transferFrom(aliceContract, bob, transferAmount); + + assertEq(cometWrapper.balanceOf(aliceContract), aliceBalance - transferAmount); + assertEq(cometWrapper.availableBalanceOf(aliceContract), aliceBalance - encumbranceAmount); + assertEq(cometWrapper.encumberedBalanceOf(aliceContract), encumbranceAmount - transferAmount); + assertEq(cometWrapper.encumbrances(aliceContract, bob), encumbranceAmount - transferAmount); + + // bob tries to reuse the same signature twice + vm.expectRevert(CometWrapper.BadSignatory.selector); + cometWrapper.encumberBySig(aliceContract, bob, encumbranceAmount, expiry, v, r, s); + + // no new encumbrance is created + assertEq(cometWrapper.balanceOf(aliceContract), aliceBalance - transferAmount); + assertEq(cometWrapper.availableBalanceOf(aliceContract), aliceBalance - encumbranceAmount); + assertEq(cometWrapper.encumberedBalanceOf(aliceContract), encumbranceAmount - transferAmount); + assertEq(cometWrapper.encumbrances(aliceContract, bob), encumbranceAmount - transferAmount); + + // alice's contract's nonce is not incremented a second time + assertEq(cometWrapper.nonces(aliceContract), nonce + 1); + + vm.stopPrank(); + } + + function test_encumberBySig_revertsForExpiredSignatureEIP1271() public { + uint256 aliceBalance = 100e18; + uint256 encumbranceAmount = 60e18; + + // alice's contract has 100 wrapped tokens + deal(address(cometWrapper), aliceContract, aliceBalance); + + assertEq(cometWrapper.balanceOf(aliceContract), aliceBalance); + assertEq(cometWrapper.availableBalanceOf(aliceContract), aliceBalance); + assertEq(cometWrapper.encumberedBalanceOf(aliceContract), 0); + assertEq(cometWrapper.encumbrances(aliceContract, bob), 0); + + uint256 nonce = cometWrapper.nonces(aliceContract); + // Fix for via-IR issue: https://github.com/foundry-rs/foundry/issues/3312#issuecomment-1255264273 + uint256 expiry = uint248(block.timestamp + 1000); + + (uint8 v, bytes32 r, bytes32 s) = aliceContractEncumberAuthorization(encumbranceAmount, nonce, expiry); + + // the expiry block arrives + vm.warp(expiry); + + // bob calls encumberBySig with the signature after the expiry + vm.prank(bob); + vm.expectRevert(CometWrapper.SignatureExpired.selector); + cometWrapper.encumberBySig(aliceContract, bob, encumbranceAmount, expiry, v, r, s); + + // no encumbrance is created + assertEq(cometWrapper.balanceOf(aliceContract), aliceBalance); + assertEq(cometWrapper.availableBalanceOf(aliceContract), aliceBalance); + assertEq(cometWrapper.encumberedBalanceOf(aliceContract), 0); + assertEq(cometWrapper.encumbrances(aliceContract, bob), 0); + + // alice's contract's nonce is not incremented + assertEq(cometWrapper.nonces(aliceContract), nonce); + } + + function test_encumberBySig_revertsInvalidVEIP1271() public { + uint256 aliceBalance = 100e18; + uint256 encumbranceAmount = 60e18; + + // alice's contract has 100 wrapped tokens + deal(address(cometWrapper), aliceContract, aliceBalance); + + assertEq(cometWrapper.balanceOf(aliceContract), aliceBalance); + assertEq(cometWrapper.availableBalanceOf(aliceContract), aliceBalance); + assertEq(cometWrapper.encumberedBalanceOf(aliceContract), 0); + assertEq(cometWrapper.encumbrances(aliceContract, bob), 0); + + uint256 nonce = cometWrapper.nonces(aliceContract); + uint256 expiry = block.timestamp + 1000; + + (, bytes32 r, bytes32 s) = aliceContractEncumberAuthorization(encumbranceAmount, nonce, expiry); + uint8 invalidV = 26; + + // bob calls encumberBySig with the signature with an invalid `v` value + vm.prank(bob); + vm.expectRevert(CometWrapper.EIP1271VerificationFailed.selector); + cometWrapper.encumberBySig(aliceContract, bob, encumbranceAmount, expiry, invalidV, r, s); + + // no encumbrance is created + assertEq(cometWrapper.balanceOf(aliceContract), aliceBalance); + assertEq(cometWrapper.availableBalanceOf(aliceContract), aliceBalance); + assertEq(cometWrapper.encumberedBalanceOf(aliceContract), 0); + assertEq(cometWrapper.encumbrances(aliceContract, bob), 0); + + // alice's contract's nonce is not incremented + assertEq(cometWrapper.nonces(aliceContract), nonce); + } + + function test_encumberBySig_revertsInvalidSEIP1271() public { + uint256 aliceBalance = 100e18; + uint256 encumbranceAmount = 60e18; + + // alice's contract has 100 wrapped tokens + deal(address(cometWrapper), aliceContract, aliceBalance); + + assertEq(cometWrapper.balanceOf(aliceContract), aliceBalance); + assertEq(cometWrapper.availableBalanceOf(aliceContract), aliceBalance); + assertEq(cometWrapper.encumberedBalanceOf(aliceContract), 0); + assertEq(cometWrapper.encumbrances(aliceContract, bob), 0); + + uint256 nonce = cometWrapper.nonces(aliceContract); + uint256 expiry = block.timestamp + 1000; + + (uint8 v, bytes32 r, ) = aliceContractEncumberAuthorization(encumbranceAmount, nonce, expiry); + + // 1 greater than the max value of s + bytes32 invalidS = 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A1; + + // bob calls encumberBySig with the signature, but he manipulates the expiry + vm.prank(bob); + vm.expectRevert(CometWrapper.EIP1271VerificationFailed.selector); + cometWrapper.encumberBySig(aliceContract, bob, encumbranceAmount, expiry, v, r, invalidS); + + // no encumbrance is created + assertEq(cometWrapper.balanceOf(aliceContract), aliceBalance); + assertEq(cometWrapper.availableBalanceOf(aliceContract), aliceBalance); + assertEq(cometWrapper.encumberedBalanceOf(aliceContract), 0); + assertEq(cometWrapper.encumbrances(aliceContract, bob), 0); + + // alice's contract's nonce is not incremented + assertEq(cometWrapper.nonces(aliceContract), nonce); + } +} diff --git a/test/CoreTest.sol b/test/CoreTest.sol index 547ba7f..e7717a7 100644 --- a/test/CoreTest.sol +++ b/test/CoreTest.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.21; import { Test } from "forge-std/Test.sol"; import { CometWrapper, CometInterface, ICometRewards, CometHelpers, ERC20 } from "../src/CometWrapper.sol"; +import { EIP1271Signer } from "../src/test/EIP1271Signer.sol"; abstract contract CoreTest is Test { function NETWORK() external virtual returns (string calldata); @@ -34,10 +35,13 @@ abstract contract CoreTest is Test { address public wrapperAddress; uint256 public decimalScale; - address alice = address(0xABCD); + uint256 alicePrivateKey = 0xa11ce; + address alice = vm.addr(alicePrivateKey); address bob = address(0xDCBA); address charlie = address(0xCDAB); + address aliceContract; // contract that can verify EIP1271 signatures + function setUp() public virtual { vm.label(alice, "alice"); vm.label(bob, "bob"); @@ -61,6 +65,7 @@ abstract contract CoreTest is Test { new CometWrapper(ERC20(cometAddress), ICometRewards(rewardAddress), "Wrapped Comet UNDERLYING", "WcUNDERLYINGv3"); wrapperAddress = address(cometWrapper); decimalScale = 10 ** underlyingToken.decimals(); + aliceContract = address(new EIP1271Signer(alice)); } function setUpFuzzTestAssumptions(uint256 amount) public view returns (uint256) { diff --git a/test/MainnetUSDCTest.t.sol b/test/MainnetUSDCTest.t.sol index 9193159..c73d9b9 100644 --- a/test/MainnetUSDCTest.t.sol +++ b/test/MainnetUSDCTest.t.sol @@ -3,12 +3,13 @@ pragma solidity 0.8.21; import { Test } from "forge-std/Test.sol"; import { CometWrapper, CometInterface, ICometRewards, CometHelpers, ERC20 } from "../src/CometWrapper.sol"; +import { BySigTest } from "./BySig.t.sol"; import { CometWrapperTest } from "./CometWrapper.t.sol"; import { CometWrapperInvariantTest } from "./CometWrapperInvariant.t.sol"; import { EncumberTest } from "./Encumber.t.sol"; import { RewardsTest } from "./Rewards.t.sol"; -contract MainnetUSDCTest is CometWrapperTest, CometWrapperInvariantTest, EncumberTest, RewardsTest { +contract MainnetUSDCTest is CometWrapperTest, CometWrapperInvariantTest, EncumberTest, RewardsTest, BySigTest { string public override NETWORK = "mainnet"; uint256 public override FORK_BLOCK_NUMBER = 16617900; diff --git a/test/MainnetWETHTest.t.sol b/test/MainnetWETHTest.t.sol index b0e9e88..3521b60 100644 --- a/test/MainnetWETHTest.t.sol +++ b/test/MainnetWETHTest.t.sol @@ -3,12 +3,13 @@ pragma solidity 0.8.21; import { Test } from "forge-std/Test.sol"; import { CometWrapper, CometInterface, ICometRewards, CometHelpers, ERC20 } from "../src/CometWrapper.sol"; +import { BySigTest } from "./BySig.t.sol"; import { CometWrapperTest } from "./CometWrapper.t.sol"; import { CometWrapperInvariantTest } from "./CometWrapperInvariant.t.sol"; import { EncumberTest } from "./Encumber.t.sol"; import { RewardsTest } from "./Rewards.t.sol"; -contract MainnetWETHTest is CometWrapperTest, CometWrapperInvariantTest, EncumberTest, RewardsTest { +contract MainnetWETHTest is CometWrapperTest, CometWrapperInvariantTest, EncumberTest, RewardsTest, BySigTest { string public override NETWORK = "mainnet"; uint256 public override FORK_BLOCK_NUMBER = 18285773;