diff --git a/src/CometWrapper.sol b/src/CometWrapper.sol index bce97af..6e23412 100644 --- a/src/CometWrapper.sol +++ b/src/CometWrapper.sol @@ -7,13 +7,14 @@ import { SafeTransferLib } from "solmate/utils/SafeTransferLib.sol"; import { CometInterface, TotalsBasic } from "./vendor/CometInterface.sol"; import { CometHelpers } from "./CometHelpers.sol"; import { ICometRewards } from "./vendor/ICometRewards.sol"; +import { IERC7246 } from "./vendor/IERC7246.sol"; /** * @title Comet Wrapper * @notice Wrapper contract that adds ERC4626 and ERC7246 functionality to the rebasing Comet token (e.g. cUSDCv3) * @author Compound & gjaldon */ -contract CometWrapper is ERC4626, CometHelpers { +contract CometWrapper is ERC4626, IERC7246, CometHelpers { using SafeTransferLib for ERC20; struct UserBasic { @@ -27,6 +28,12 @@ contract CometWrapper is ERC4626, CometHelpers { /// @notice Mapping of users to their rewards claimed mapping(address => uint256) public rewardsClaimed; + /// @notice Amount of an address's token balance that is encumbered + mapping (address => uint256) public encumberedBalanceOf; + + /// @notice Amount encumbered from owner to taker (owner => taker => balance) + mapping (address => mapping (address => uint256)) public encumbrances; + /// @notice The Comet address that this contract wraps CometInterface public immutable comet; @@ -42,6 +49,8 @@ contract CometWrapper is ERC4626, CometHelpers { /** Custom errors **/ error InsufficientAllowance(); + error InsufficientAvailableBalance(); + error InsufficientEncumbrance(); error TimestampTooLarge(); error UninitializedReward(); error ZeroShares(); @@ -136,11 +145,8 @@ contract CometWrapper is ERC4626, CometHelpers { if (shares == 0) revert ZeroShares(); if (msg.sender != owner) { - uint256 allowed = allowance[owner][msg.sender]; - if (allowed < shares) revert InsufficientAllowance(); - if (allowed != type(uint256).max) { - allowance[owner][msg.sender] = allowed - shares; - } + // TODO: spend encumbrance, then allowance + spendAllowanceInternal(owner, msg.sender, shares); } _burn(owner, shares); @@ -162,11 +168,8 @@ contract CometWrapper is ERC4626, CometHelpers { function redeem(uint256 shares, address receiver, address owner) public override returns (uint256) { if (shares == 0) revert ZeroShares(); if (msg.sender != owner) { - uint256 allowed = allowance[owner][msg.sender]; - if (allowed < shares) revert InsufficientAllowance(); - if (allowed != type(uint256).max) { - allowance[owner][msg.sender] = allowed - shares; - } + // TODO: spend encumbrance, then allowance + spendAllowanceInternal(owner, msg.sender, shares); } accrueInternal(owner); @@ -182,27 +185,42 @@ contract CometWrapper is ERC4626, CometHelpers { /** * @notice Transfer shares from caller to the recipient - * @param to The receiver of the shares (Wrapped Comet) to be transferred + * @dev Confirms the available balance of the caller is sufficient to cover transfer + * @param to The receiver of the shares to be transferred * @param amount The amount of shares to be transferred * @return bool Indicates success of the transfer */ function transfer(address to, uint256 amount) public override returns (bool) { + if (availableBalanceOf(msg.sender) < amount) revert InsufficientAvailableBalance(); transferInternal(msg.sender, to, amount); return true; } /** - * @notice Transfer shares from a specified source to a recipient + * @notice Transfer shares from a specified source to a recipient using the encumbrance and allowance of the caller + * @dev Spends the caller's encumbrance from `from` first, then their allowance from `from` (if necessary) * @param from The source of the shares to be transferred - * @param to The receiver of the shares (Wrapped Comet) to be transferred + * @param to The receiver of the shares to be transferred * @param amount The amount of shares to be transferred * @return bool Indicates success of the transfer */ function transferFrom(address from, address to, uint256 amount) public override returns (bool) { - uint256 allowed = msg.sender == from ? type(uint256).max : allowance[from][msg.sender]; - if (allowed < amount) revert InsufficientAllowance(); - if (allowed != type(uint256).max) { - allowance[from][msg.sender] = allowed - amount; + uint256 encumberedToTaker = encumbrances[from][msg.sender]; + if (amount > encumberedToTaker) { + uint256 excessAmount = amount - encumberedToTaker; + + // WARNING: this check needs to happen BEFORE releaseEncumbranceInternal, + // otherwise the released encumbrance will increase availableBalanceOf(from), + // allowing msg.sender to transfer tokens that are encumbered to someone else + + if (availableBalanceOf(from) < excessAmount) revert InsufficientAvailableBalance(); + + // Exceeds Encumbrance, so spend all of it + releaseEncumbranceInternal(from, msg.sender, encumberedToTaker); + + spendAllowanceInternal(from, msg.sender, excessAmount); + } else { + releaseEncumbranceInternal(from, msg.sender, amount); } transferInternal(from, to, amount); @@ -270,7 +288,8 @@ contract CometWrapper is ERC4626, CometHelpers { * @notice Get the reward owed to an account * @dev This is designed to exactly match computation of rewards in Comet * and uses the same configuration as CometRewards. It is a combination of both - * [`getRewardOwed`](https://github.com/compound-finance/comet/blob/63e98e5d231ef50c755a9489eb346a561fc7663c/contracts/CometRewards.sol#L110) and [`getRewardAccrued`](https://github.com/compound-finance/comet/blob/63e98e5d231ef50c755a9489eb346a561fc7663c/contracts/CometRewards.sol#L171). + * [`getRewardOwed`](https://github.com/compound-finance/comet/blob/63e98e5d231ef50c755a9489eb346a561fc7663c/contracts/CometRewards.sol#L110) + * and [`getRewardAccrued`](https://github.com/compound-finance/comet/blob/63e98e5d231ef50c755a9489eb346a561fc7663c/contracts/CometRewards.sol#L171). * @param account The address to be queried * @return The total amount of rewards owed to an account */ @@ -473,4 +492,92 @@ contract CometWrapper is ERC4626, CometHelpers { if (block.timestamp >= 2**40) revert TimestampTooLarge(); return uint40(block.timestamp); } + + /** + * @dev Updates `owner` s allowance for `spender` based on spent `amount`. + * + * Does not update the allowance amount in case of infinite allowance. + * Revert if not enough allowance is available. + * + * Might emit an {Approval} event. + */ + function spendAllowanceInternal( + address owner, + address spender, + uint256 amount + ) internal virtual { + uint256 allowed = allowance[owner][spender]; + if (allowed < amount) revert InsufficientAllowance(); + if (allowed != type(uint256).max) { + allowance[owner][spender] = allowed - amount; + emit Approval(owner, spender, allowed - amount); + } + } + + /** ERC7246 Functions **/ + + /** + * @notice Amount of an address's token balance that is not encumbered + * @param owner Address to check the available balance of + * @return uint256 Unencumbered balance + */ + function availableBalanceOf(address owner) public view returns (uint256) { + return (balanceOf[owner] - encumberedBalanceOf[owner]); + } + + /** + * @notice Increases the amount of tokens that the caller has encumbered to + * `taker` by `amount` + * @param taker Address to increase encumbrance to + * @param amount Amount of tokens to increase the encumbrance by + */ + function encumber(address taker, uint256 amount) external { + encumberInternal(msg.sender, taker, amount); + } + + /** + * @dev Increase `owner`'s encumbrance to `taker` by `amount` + */ + function encumberInternal(address owner, address taker, uint256 amount) private { + if (availableBalanceOf(owner) < amount) revert InsufficientAvailableBalance(); + encumbrances[owner][taker] += amount; + encumberedBalanceOf[owner] += amount; + emit Encumber(owner, taker, amount); + } + + /** + * @notice Increases the amount of tokens that `owner` has encumbered to + * `taker` by `amount`. + * @dev Spends the caller's `allowance` + * @param owner Address to increase encumbrance from + * @param taker Address to increase encumbrance to + * @param amount Amount of tokens to increase the encumbrance to `taker` by + */ + function encumberFrom(address owner, address taker, uint256 amount) external { + if (allowance[owner][msg.sender] < amount) revert InsufficientAllowance(); + spendAllowanceInternal(owner, msg.sender, amount); + encumberInternal(owner, taker , amount); + } + + /** + * @notice Reduces amount of tokens encumbered from `owner` to caller by + * `amount` + * @dev Spends all of the encumbrance if `amount` is greater than `owner`'s + * current encumbrance to caller + * @param owner Address to decrease encumbrance from + * @param amount Amount of tokens to decrease the encumbrance by + */ + function release(address owner, uint256 amount) external { + releaseEncumbranceInternal(owner, msg.sender, amount); + } + + /** + * @dev Reduce `owner`'s encumbrance to `taker` by `amount` + */ + function releaseEncumbranceInternal(address owner, address taker, uint256 amount) private { + if (encumbrances[owner][taker] < amount) revert InsufficientEncumbrance(); + encumbrances[owner][taker] -= amount; + encumberedBalanceOf[owner] -= amount; + emit Release(owner, taker, amount); + } } diff --git a/src/vendor/CometExtInterface.sol b/src/vendor/CometExtInterface.sol index 836c8c4..5e3e2e3 100644 --- a/src/vendor/CometExtInterface.sol +++ b/src/vendor/CometExtInterface.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: BUSL-1.1 +// SPDX-License-Identifier: MIT pragma solidity 0.8.21; struct TotalsBasic { diff --git a/src/vendor/CometInterface.sol b/src/vendor/CometInterface.sol index e72456d..7138d57 100644 --- a/src/vendor/CometInterface.sol +++ b/src/vendor/CometInterface.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: BUSL-1.1 +// SPDX-License-Identifier: MIT pragma solidity 0.8.21; import "./CometMainInterface.sol"; diff --git a/src/vendor/CometMainInterface.sol b/src/vendor/CometMainInterface.sol index 8ac65ed..487015d 100644 --- a/src/vendor/CometMainInterface.sol +++ b/src/vendor/CometMainInterface.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: BUSL-1.1 +// SPDX-License-Identifier: MIT pragma solidity 0.8.21; struct AssetInfo { diff --git a/src/vendor/CometMath.sol b/src/vendor/CometMath.sol index 4baab1e..4d326fb 100644 --- a/src/vendor/CometMath.sol +++ b/src/vendor/CometMath.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: BUSL-1.1 +// SPDX-License-Identifier: MIT pragma solidity 0.8.21; /** diff --git a/src/vendor/ICometRewards.sol b/src/vendor/ICometRewards.sol index 0c93bd4..02c35a5 100644 --- a/src/vendor/ICometRewards.sol +++ b/src/vendor/ICometRewards.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: ISC +// SPDX-License-Identifier: MIT pragma solidity 0.8.21; interface ICometRewards { diff --git a/src/vendor/IERC7246.sol b/src/vendor/IERC7246.sol new file mode 100644 index 0000000..3b5e9f8 --- /dev/null +++ b/src/vendor/IERC7246.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +/** + * @dev Interface of the ERC7246 standard. + */ +interface IERC7246 { + /** + * @dev Emitted when `amount` tokens are encumbered from `owner` to `taker`. + */ + event Encumber(address indexed owner, address indexed taker, uint256 amount); + + /** + * @dev Emitted when the encumbrance of an `owner` to a `taker` is reduced + * by `amount`. + */ + event Release(address indexed owner, address indexed taker, uint256 amount); + + /** + * @dev Returns the total amount of tokens owned by `owner` that are + * currently encumbered. MUST never exceed `balanceOf(owner)` + * + * Any function which would reduce balanceOf(owner) below + * encumberedBalanceOf(owner) MUST revert + */ + function encumberedBalanceOf(address owner) external view returns (uint256); + + /** + * @dev Returns the number of tokens that `owner` has encumbered to `taker`. + * + * This value increases when {encumber} or {encumberFrom} are called by the + * `owner` or by another permitted account. + * This value decreases when {release} and {transferFrom} are called by + * `taker`. + */ + function encumbrances(address owner, address taker) external view returns (uint256); + + /** + * @dev Increases the amount of tokens that the caller has encumbered to + * `taker` by `amount`. + * Grants to `taker` a guaranteed right to transfer `amount` from the + * caller's balance by using `transferFrom`. + * + * MUST revert if caller does not have `amount` tokens available (e.g. if + * `balanceOf(caller) - encumbrances(caller) < amount`). + * + * Emits an {Encumber} event. + */ + function encumber(address taker, uint256 amount) external; + + /** + * @dev Increases the amount of tokens that `owner` has encumbered to + * `taker` by `amount`. + * Grants to `taker` a guaranteed right to transfer `amount` from `owner` + * using transferFrom + * + * The function SHOULD revert unless the owner account has deliberately + * authorized the sender of the message via some mechanism. + * + * MUST revert if `owner` does not have `amount` tokens available (e.g. if + * `balanceOf(owner) - encumbrances(owner) < amount`). + * + * Emits an {Encumber} event. + */ + function encumberFrom(address owner, address taker, uint256 amount) external; + + /** + * @dev Reduces amount of tokens encumbered from `owner` to caller by + * `amount`. + * + * Emits a {Release} event. + */ + function release(address owner, uint256 amount) external; + + /** + * @dev Convenience function for reading the unencumbered balance of an address. + * Trivially implemented as `balanceOf(owner) - encumberedBalanceOf(owner)` + */ + function availableBalanceOf(address owner) external view returns (uint256); +} diff --git a/test/BaseUSDbCTest.t.sol b/test/BaseUSDbCTest.t.sol index 99ca305..4996e79 100644 --- a/test/BaseUSDbCTest.t.sol +++ b/test/BaseUSDbCTest.t.sol @@ -5,9 +5,10 @@ import { Test } from "forge-std/Test.sol"; import { CometWrapper, CometInterface, ICometRewards, CometHelpers, ERC20 } from "../src/CometWrapper.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, RewardsTest { +contract BaseUSDbCTest is CometWrapperTest, CometWrapperInvariantTest, EncumberTest, RewardsTest { string public override NETWORK = "base"; uint256 public override FORK_BLOCK_NUMBER = 4791144; diff --git a/test/CometWrapper.t.sol b/test/CometWrapper.t.sol index 4b88c5a..d346911 100644 --- a/test/CometWrapper.t.sol +++ b/test/CometWrapper.t.sol @@ -849,7 +849,7 @@ abstract contract CometWrapperTest is CoreTest, CometMath { vm.startPrank(alice); comet.allow(wrapperAddress, true); cometWrapper.mint(9_000 * decimalScale, alice); - cometWrapper.transferFrom(alice, bob, 1_337 * decimalScale); + cometWrapper.transfer(bob, 1_337 * decimalScale); vm.stopPrank(); assertEq(cometWrapper.balanceOf(alice), 7_663 * decimalScale); @@ -888,7 +888,13 @@ abstract contract CometWrapperTest is CoreTest, CometMath { vm.startPrank(alice); comet.allow(wrapperAddress, true); cometWrapper.mint(5_000 * decimalScale, alice); + // Alice needs to give approval to herself in order to `transferFrom` + vm.expectEmit(true, true, true, true); + emit Approval(alice, alice, 2_500 * decimalScale); + cometWrapper.approve(alice, 2_500 * decimalScale); + vm.expectEmit(true, true, true, true); + emit Approval(alice, alice, 0); cometWrapper.transferFrom(alice, bob, 2_500 * decimalScale); vm.stopPrank(); @@ -910,11 +916,15 @@ abstract contract CometWrapperTest is CoreTest, CometMath { cometWrapper.transferFrom(alice, bob, 5_000 * decimalScale); vm.prank(alice); + vm.expectEmit(true, true, true, true); + emit Approval(alice, bob, 2_700 * decimalScale); cometWrapper.approve(bob, 2_700 * decimalScale); vm.startPrank(bob); // Allowances should be updated when transferFrom is done assertEq(cometWrapper.allowance(alice, bob), 2_700 * decimalScale); + vm.expectEmit(true, true, true, true); + emit Approval(alice, bob, 200 * decimalScale); cometWrapper.transferFrom(alice, bob, 2_500 * decimalScale); assertEq(cometWrapper.balanceOf(alice), 2_500 * decimalScale); assertEq(cometWrapper.balanceOf(bob), 2_500 * decimalScale); @@ -927,6 +937,8 @@ abstract contract CometWrapperTest is CoreTest, CometMath { // Infinite allowance does not decrease allowance vm.prank(bob); + vm.expectEmit(true, true, true, true); + emit Approval(bob, alice, type(uint256).max); cometWrapper.approve(alice, type(uint256).max); assertEq(cometWrapper.allowance(bob, alice), type(uint256).max); diff --git a/test/CometWrapperInvariant.t.sol b/test/CometWrapperInvariant.t.sol index fa3a94a..2042742 100644 --- a/test/CometWrapperInvariant.t.sol +++ b/test/CometWrapperInvariant.t.sol @@ -154,25 +154,25 @@ abstract contract CometWrapperInvariantTest is CoreTest, CometMath { for (uint256 i; i < 5; i++) { vm.startPrank(alice); - cometWrapper.transferFrom(alice, bob, cometWrapper.balanceOf(alice)/5); + cometWrapper.transfer(bob, cometWrapper.balanceOf(alice)/5); assertEq(cometWrapper.totalAssets(), totalAssets); assertEq(cometWrapper.totalSupply(), totalSupply); vm.stopPrank(); vm.startPrank(bob); - cometWrapper.transferFrom(bob, alice, cometWrapper.balanceOf(bob)/5); + cometWrapper.transfer(alice, cometWrapper.balanceOf(bob)/5); assertEq(cometWrapper.totalAssets(), totalAssets); assertEq(cometWrapper.totalSupply(), totalSupply); vm.stopPrank(); vm.startPrank(bob); - cometWrapper.transferFrom(bob, alice, cometWrapper.balanceOf(bob)/5); + cometWrapper.transfer(alice, cometWrapper.balanceOf(bob)/5); assertEq(cometWrapper.totalAssets(), totalAssets); assertEq(cometWrapper.totalSupply(), totalSupply); vm.stopPrank(); vm.startPrank(alice); - cometWrapper.transferFrom(alice, bob, cometWrapper.balanceOf(alice)/5); + cometWrapper.transfer(bob, cometWrapper.balanceOf(alice)/5); assertEq(cometWrapper.totalAssets(), totalAssets); assertEq(cometWrapper.totalSupply(), totalSupply); vm.stopPrank(); diff --git a/test/CoreTest.sol b/test/CoreTest.sol index ea86764..547ba7f 100644 --- a/test/CoreTest.sol +++ b/test/CoreTest.sol @@ -36,10 +36,12 @@ abstract contract CoreTest is Test { address alice = address(0xABCD); address bob = address(0xDCBA); + address charlie = address(0xCDAB); function setUp() public virtual { vm.label(alice, "alice"); vm.label(bob, "bob"); + vm.label(charlie, "charlie"); vm.createSelectFork(vm.rpcUrl(this.NETWORK()), this.FORK_BLOCK_NUMBER()); cometAddress = this.COMET_ADDRESS(); diff --git a/test/Encumber.t.sol b/test/Encumber.t.sol new file mode 100644 index 0000000..0543a30 --- /dev/null +++ b/test/Encumber.t.sol @@ -0,0 +1,322 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import { CoreTest, CometHelpers, CometWrapper, ERC20, ICometRewards } from "./CoreTest.sol"; + +abstract contract EncumberTest is CoreTest { + event Encumber(address indexed owner, address indexed taker, uint amount); + event Release(address indexed owner, address indexed taker, uint amount); + + function test_availableBalanceOf() public { + vm.startPrank(alice); + + // availableBalanceOf is 0 by default + assertEq(cometWrapper.availableBalanceOf(alice), 0); + + // reflects balance when there are no encumbrances + deal(address(cometWrapper), alice, 100e18); + assertEq(cometWrapper.balanceOf(alice), 100e18); + assertEq(cometWrapper.availableBalanceOf(alice), 100e18); + + // is reduced by encumbrances + cometWrapper.encumber(bob, 20e18); + assertEq(cometWrapper.balanceOf(alice), 100e18); + assertEq(cometWrapper.availableBalanceOf(alice), 80e18); + + // is reduced by transfers + cometWrapper.transfer(bob, 20e18); + assertEq(cometWrapper.balanceOf(alice), 80e18); + assertEq(cometWrapper.availableBalanceOf(alice), 60e18); + + vm.stopPrank(); + + vm.startPrank(bob); + + // is NOT reduced by transferFrom (from an encumbered address) + cometWrapper.transferFrom(alice, charlie, 10e18); + assertEq(cometWrapper.balanceOf(alice), 70e18); + assertEq(cometWrapper.availableBalanceOf(alice), 60e18); + assertEq(cometWrapper.encumbrances(alice, bob), 10e18); + assertEq(cometWrapper.balanceOf(charlie), 10e18); + + // is increased by a release + cometWrapper.release(alice, 5e18); + assertEq(cometWrapper.balanceOf(alice), 70e18); + assertEq(cometWrapper.availableBalanceOf(alice), 65e18); + assertEq(cometWrapper.encumbrances(alice, bob), 5e18); + + vm.stopPrank(); + } + + function test_transfer_revertsOnInsufficentAvailableBalance() public { + deal(address(cometWrapper), alice, 100e18); + vm.startPrank(alice); + + // alice encumbers half her balance to bob + cometWrapper.encumber(bob, 50e18); + + // alice attempts to transfer her entire balance + vm.expectRevert(CometWrapper.InsufficientAvailableBalance.selector); + cometWrapper.transfer(charlie, 100e18); + + vm.stopPrank(); + } + + function test_encumber_revertsOnInsufficientAvailableBalance() public { + deal(address(cometWrapper), alice, 100e18); + vm.startPrank(alice); + + // alice encumbers half her balance to bob + cometWrapper.encumber(bob, 50e18); + + // alice attempts to encumber more than her remaining available balance + vm.expectRevert(CometWrapper.InsufficientAvailableBalance.selector); + cometWrapper.encumber(charlie, 60e18); + + vm.stopPrank(); + } + + function test_encumber() public { + deal(address(cometWrapper), alice, 100e18); + vm.startPrank(alice); + + // emits Encumber event + vm.expectEmit(true, true, true, true); + emit Encumber(alice, bob, 60e18); + + // alice encumbers some of her balance to bob + cometWrapper.encumber(bob, 60e18); + + // balance is unchanged + assertEq(cometWrapper.balanceOf(alice), 100e18); + // available balance is reduced + assertEq(cometWrapper.availableBalanceOf(alice), 40e18); + + // creates encumbrance for taker + assertEq(cometWrapper.encumbrances(alice, bob), 60e18); + + // updates encumbered balance of owner + assertEq(cometWrapper.encumberedBalanceOf(alice), 60e18); + } + + function test_transferFromWithSufficientEncumbrance() public { + deal(address(cometWrapper), alice, 100e18); + vm.prank(alice); + + // alice encumbers some of her balance to bob + cometWrapper.encumber(bob, 60e18); + + assertEq(cometWrapper.balanceOf(alice), 100e18); + assertEq(cometWrapper.availableBalanceOf(alice), 40e18); + assertEq(cometWrapper.encumberedBalanceOf(alice), 60e18); + assertEq(cometWrapper.encumbrances(alice, bob), 60e18); + assertEq(cometWrapper.balanceOf(charlie), 0); + + // bob calls transfers from alice to charlie + vm.prank(bob); + vm.expectEmit(true, true, true, true); + emit Release(alice, bob, 40e18); + cometWrapper.transferFrom(alice, charlie, 40e18); + + // alice balance is reduced + assertEq(cometWrapper.balanceOf(alice), 60e18); + // alice encumbrance to bob is reduced + assertEq(cometWrapper.availableBalanceOf(alice), 40e18); + assertEq(cometWrapper.encumberedBalanceOf(alice), 20e18); + assertEq(cometWrapper.encumbrances(alice, bob), 20e18); + // transfer is completed + assertEq(cometWrapper.balanceOf(charlie), 40e18); + } + + function test_transferFromUsesEncumbranceAndAllowance() public { + deal(address(cometWrapper), alice, 100e18); + vm.startPrank(alice); + + // alice encumbers some of her balance to bob + cometWrapper.encumber(bob, 20e18); + + // she also grants him an approval + cometWrapper.approve(bob, 30e18); + + vm.stopPrank(); + + assertEq(cometWrapper.balanceOf(alice), 100e18); + assertEq(cometWrapper.availableBalanceOf(alice), 80e18); + assertEq(cometWrapper.encumberedBalanceOf(alice), 20e18); + assertEq(cometWrapper.encumbrances(alice, bob), 20e18); + assertEq(cometWrapper.allowance(alice, bob), 30e18); + assertEq(cometWrapper.balanceOf(charlie), 0); + + // bob calls transfers from alice to charlie + vm.prank(bob); + vm.expectEmit(true, true, true, true); + emit Release(alice, bob, 20e18); + cometWrapper.transferFrom(alice, charlie, 40e18); + + // alice balance is reduced + assertEq(cometWrapper.balanceOf(alice), 60e18); + + // her encumbrance to bob has been fully spent + assertEq(cometWrapper.availableBalanceOf(alice), 60e18); + assertEq(cometWrapper.encumberedBalanceOf(alice), 0); + assertEq(cometWrapper.encumbrances(alice, bob), 0); + + // her allowance to bob has been partially spent + assertEq(cometWrapper.allowance(alice, bob), 10e18); + + // the dst receives the transfer + assertEq(cometWrapper.balanceOf(charlie), 40e18); + } + + function test_transferFrom_revertsIfSpendingTokensEncumberedToOthers() public { + deal(address(cometWrapper), alice, 200e18); + vm.startPrank(alice); + + // alice encumbers some of her balance to bob + cometWrapper.encumber(bob, 50e18); + + // she also grants him an approval + cometWrapper.approve(bob, type(uint256).max); + + // alice encumbers the remainder of her balance to charlie + cometWrapper.encumber(charlie, 150e18); + + vm.stopPrank(); + + assertEq(cometWrapper.balanceOf(alice), 200e18); + assertEq(cometWrapper.availableBalanceOf(alice), 0); + assertEq(cometWrapper.encumberedBalanceOf(alice), 200e18); + assertEq(cometWrapper.encumbrances(alice, bob), 50e18); + assertEq(cometWrapper.encumbrances(alice, charlie), 150e18); + assertEq(cometWrapper.allowance(alice, bob), type(uint256).max); + + // bob calls transfers from alice, attempting to transfer his encumbered + // tokens and also transfer tokens encumbered to charlie + vm.prank(bob); + vm.expectRevert(CometWrapper.InsufficientAvailableBalance.selector); + cometWrapper.transferFrom(alice, bob, 100e18); + } + + function test_transferFrom_revertsIfInsufficientAllowance() public { + deal(address(cometWrapper), alice, 100e18); + + vm.startPrank(alice); + + // alice encumbers some of her balance to bob + cometWrapper.encumber(bob, 10e18); + + // she also grants him an approval + cometWrapper.approve(bob, 20e18); + + vm.stopPrank(); + + assertEq(cometWrapper.balanceOf(alice), 100e18); + assertEq(cometWrapper.availableBalanceOf(alice), 90e18); + assertEq(cometWrapper.encumberedBalanceOf(alice), 10e18); + assertEq(cometWrapper.encumbrances(alice, bob), 10e18); + assertEq(cometWrapper.allowance(alice, bob), 20e18); + assertEq(cometWrapper.balanceOf(charlie), 0); + + // bob tries to transfer more than his encumbered and allowed balances + vm.prank(bob); + vm.expectRevert(CometWrapper.InsufficientAllowance.selector); + cometWrapper.transferFrom(alice, charlie, 40e18); + } + + function test_encumberFrom_revertsOnInsufficientAllowance() public { + deal(address(cometWrapper), alice, 100e18); + + // alice grants bob an approval + vm.prank(alice); + cometWrapper.approve(bob, 50e18); + + // but bob tries to encumber more than his allowance + vm.prank(bob); + vm.expectRevert(CometWrapper.InsufficientAllowance.selector); + cometWrapper.encumberFrom(alice, charlie, 60e18); + } + + function test_encumberFrom() public { + deal(address(cometWrapper), alice, 100e18); + + // alice grants bob an approval + vm.prank(alice); + cometWrapper.approve(bob, 100e18); + + assertEq(cometWrapper.balanceOf(alice), 100e18); + assertEq(cometWrapper.availableBalanceOf(alice), 100e18); + assertEq(cometWrapper.encumberedBalanceOf(alice), 0e18); + assertEq(cometWrapper.encumbrances(alice, bob), 0e18); + assertEq(cometWrapper.allowance(alice, bob), 100e18); + assertEq(cometWrapper.balanceOf(charlie), 0); + + // bob encumbers part of his allowance from alice to charlie + vm.prank(bob); + // emits an Encumber event + vm.expectEmit(true, true, true, true); + emit Encumber(alice, charlie, 60e18); + cometWrapper.encumberFrom(alice, charlie, 60e18); + + // no balance is transferred + assertEq(cometWrapper.balanceOf(alice), 100e18); + assertEq(cometWrapper.balanceOf(charlie), 0); + // but available balance is reduced + assertEq(cometWrapper.availableBalanceOf(alice), 40e18); + // encumbrance to charlie is created + assertEq(cometWrapper.encumberedBalanceOf(alice), 60e18); + assertEq(cometWrapper.encumbrances(alice, bob), 0e18); + assertEq(cometWrapper.encumbrances(alice, charlie), 60e18); + // allowance is partially spent + assertEq(cometWrapper.allowance(alice, bob), 40e18); + } + + function test_release() public { + deal(address(cometWrapper), alice, 100e18); + + vm.prank(alice); + + // alice encumbers her balance to bob + cometWrapper.encumber(bob, 100e18); + + assertEq(cometWrapper.balanceOf(alice), 100e18); + assertEq(cometWrapper.availableBalanceOf(alice), 0); + assertEq(cometWrapper.encumberedBalanceOf(alice), 100e18); + assertEq(cometWrapper.encumbrances(alice, bob), 100e18); + + // bob releases part of the encumbrance + vm.prank(bob); + // emits Release event + vm.expectEmit(true, true, true, true); + emit Release(alice, bob, 40e18); + cometWrapper.release(alice, 40e18); + + assertEq(cometWrapper.balanceOf(alice), 100e18); + assertEq(cometWrapper.availableBalanceOf(alice), 40e18); + assertEq(cometWrapper.encumberedBalanceOf(alice), 60e18); + assertEq(cometWrapper.encumbrances(alice, bob), 60e18); + } + + function test_release_revertsOnInsufficientEncumbrance() public { + deal(address(cometWrapper), alice, 100e18); + + vm.prank(alice); + + // alice encumbers her balance to bob + cometWrapper.encumber(bob, 100e18); + + assertEq(cometWrapper.balanceOf(alice), 100e18); + assertEq(cometWrapper.availableBalanceOf(alice), 0); + assertEq(cometWrapper.encumberedBalanceOf(alice), 100e18); + assertEq(cometWrapper.encumbrances(alice, bob), 100e18); + + // bob releases a greater amount than is encumbered to him + vm.prank(bob); + vm.expectRevert(CometWrapper.InsufficientEncumbrance.selector); + cometWrapper.release(alice, 200e18); + + assertEq(cometWrapper.balanceOf(alice), 100e18); + assertEq(cometWrapper.availableBalanceOf(alice), 0); + assertEq(cometWrapper.encumberedBalanceOf(alice), 100e18); + assertEq(cometWrapper.encumbrances(alice, bob), 100e18); + } +} diff --git a/test/MainnetUSDCTest.t.sol b/test/MainnetUSDCTest.t.sol index 8f0b206..9193159 100644 --- a/test/MainnetUSDCTest.t.sol +++ b/test/MainnetUSDCTest.t.sol @@ -5,9 +5,10 @@ import { Test } from "forge-std/Test.sol"; import { CometWrapper, CometInterface, ICometRewards, CometHelpers, ERC20 } from "../src/CometWrapper.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, RewardsTest { +contract MainnetUSDCTest is CometWrapperTest, CometWrapperInvariantTest, EncumberTest, RewardsTest { 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 e7392cf..b0e9e88 100644 --- a/test/MainnetWETHTest.t.sol +++ b/test/MainnetWETHTest.t.sol @@ -5,9 +5,10 @@ import { Test } from "forge-std/Test.sol"; import { CometWrapper, CometInterface, ICometRewards, CometHelpers, ERC20 } from "../src/CometWrapper.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, RewardsTest { +contract MainnetWETHTest is CometWrapperTest, CometWrapperInvariantTest, EncumberTest, RewardsTest { string public override NETWORK = "mainnet"; uint256 public override FORK_BLOCK_NUMBER = 18285773;