Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add encumber functionality #16

Merged
merged 4 commits into from
Oct 20, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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