Skip to content

Commit

Permalink
Add encumber functionality (#16)
Browse files Browse the repository at this point in the history
This PR adds encumber (ERC7246) functionality to the Comet Wrapper. There are a couple TODOs to spend encumbrances in `withdraw` and `redeem` that will be done in a separate PR.
  • Loading branch information
kevincheng96 authored Oct 20, 2023
1 parent af706b7 commit 812b43f
Show file tree
Hide file tree
Showing 14 changed files with 558 additions and 32 deletions.
145 changes: 126 additions & 19 deletions src/CometWrapper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;

Expand All @@ -42,6 +49,8 @@ contract CometWrapper is ERC4626, CometHelpers {
/** Custom errors **/

error InsufficientAllowance();
error InsufficientAvailableBalance();
error InsufficientEncumbrance();
error TimestampTooLarge();
error UninitializedReward();
error ZeroShares();
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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);
}
}
2 changes: 1 addition & 1 deletion src/vendor/CometExtInterface.sol
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-License-Identifier: BUSL-1.1
// SPDX-License-Identifier: MIT
pragma solidity 0.8.21;

struct TotalsBasic {
Expand Down
2 changes: 1 addition & 1 deletion src/vendor/CometInterface.sol
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-License-Identifier: BUSL-1.1
// SPDX-License-Identifier: MIT
pragma solidity 0.8.21;

import "./CometMainInterface.sol";
Expand Down
2 changes: 1 addition & 1 deletion src/vendor/CometMainInterface.sol
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-License-Identifier: BUSL-1.1
// SPDX-License-Identifier: MIT
pragma solidity 0.8.21;

struct AssetInfo {
Expand Down
2 changes: 1 addition & 1 deletion src/vendor/CometMath.sol
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-License-Identifier: BUSL-1.1
// SPDX-License-Identifier: MIT
pragma solidity 0.8.21;

/**
Expand Down
2 changes: 1 addition & 1 deletion src/vendor/ICometRewards.sol
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-License-Identifier: ISC
// SPDX-License-Identifier: MIT
pragma solidity 0.8.21;

interface ICometRewards {
Expand Down
80 changes: 80 additions & 0 deletions src/vendor/IERC7246.sol
Original file line number Diff line number Diff line change
@@ -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);
}
3 changes: 2 additions & 1 deletion test/BaseUSDbCTest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
14 changes: 13 additions & 1 deletion test/CometWrapper.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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();

Expand All @@ -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);
Expand All @@ -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);

Expand Down
Loading

0 comments on commit 812b43f

Please sign in to comment.