Skip to content
This repository was archived by the owner on Jun 28, 2024. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ remappings = [
"openzeppelin/=lib/openzeppelin-contracts/contracts/",
"evc/=lib/ethereum-vault-connector/src/",
"evc-playground/=lib/evc-playground/src/",
"solmate/=lib/evc-playground/lib/solmate/src/",
"a16z-erc4626-tests/=lib/erc4626-tests/"
]

Expand Down
263 changes: 259 additions & 4 deletions src/workshop_2/WorkshopVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,35 @@ pragma solidity ^0.8.19;
import "openzeppelin/token/ERC20/extensions/ERC4626.sol";
import "evc/interfaces/IEthereumVaultConnector.sol";
import "evc/interfaces/IVault.sol";
import "solmate/utils/FixedPointMathLib.sol";
import "./IWorkshopVault.sol";

contract WorkshopVault is ERC4626, IVault, IWorkshopVault {
using FixedPointMathLib for uint256;

event Borrow(address indexed caller, address indexed owner, uint256 assets);
event Repay(address indexed caller, address indexed receiver, uint256 assets);

error ControllerDisabled();
error SharesSeizureFailed();
error NoLiquidationOpportunity();

IEVC internal immutable evc;

// Borrowing model variable
bytes private snapshot;
uint256 public totalBorrowed;
uint256 internal _totalAssets;
mapping(address account => uint256 assets) internal owed;

// Interest rate model variables
uint256 internal interestRate = 10; // 10% APY
uint256 internal lastInterestUpdate = block.timestamp;
uint256 internal constant ONE = 1e27;
uint256 internal interestAccumulator = ONE;

mapping(address account => uint256) internal userInterestAccumulator;

constructor(
IEVC _evc,
IERC20 _asset,
Expand Down Expand Up @@ -59,6 +83,7 @@ contract WorkshopVault is ERC4626, IVault, IWorkshopVault {
// IVault
// [ASSIGNMENT]: why this function is necessary? is it safe to unconditionally disable the controller?
function disableController() external {
require(_debtOf(_msgSender()) == 0);
evc.disableController(_msgSender());
}

Expand Down Expand Up @@ -92,6 +117,7 @@ contract WorkshopVault is ERC4626, IVault, IWorkshopVault {
uint256 assets,
address receiver
) public virtual override callThroughEVC withChecks(address(0)) returns (uint256 shares) {
_totalAssets += assets;
return super.deposit(assets, receiver);
}

Expand All @@ -107,6 +133,7 @@ contract WorkshopVault is ERC4626, IVault, IWorkshopVault {
address receiver,
address owner
) public virtual override callThroughEVC withChecks(owner) returns (uint256 shares) {
_totalAssets -= assets;
return super.withdraw(assets, receiver, owner);
}

Expand All @@ -133,9 +160,237 @@ contract WorkshopVault is ERC4626, IVault, IWorkshopVault {
return super.transferFrom(from, to, value);
}

function maxWithdraw(address owner) public view virtual override returns (uint256) {
return super.maxWithdraw(owner);
}

function maxRedeem(address owner) public view virtual override returns (uint256) {
return balanceOf(owner) - owed[owner];
}

function _convertToShares(uint256 assets, bool roundUp) internal view virtual returns (uint256) {
(uint256 currentTotalBorrowed,,) = _accrueInterestCalculate();

return roundUp
? assets.mulDivUp(totalSupply() + 1, _totalAssets + currentTotalBorrowed + 1)
: assets.mulDivDown(totalSupply() + 1, _totalAssets + currentTotalBorrowed + 1);
}

/// @dev This function is overridden to take into account the fact that some of the assets may be borrowed.
function _convertToAssets(uint256 shares, bool roundUp) internal view virtual returns (uint256) {
(uint256 currentTotalBorrowed,,) = _accrueInterestCalculate();
return roundUp
? shares.mulDivUp(_totalAssets + currentTotalBorrowed + 1, totalSupply() + 1)
: shares.mulDivDown(_totalAssets + currentTotalBorrowed + 1, totalSupply() + 1);
}

function convertToAssets(uint256 shares) public view virtual override returns (uint256) {
return _convertToAssets(shares, true);
}

function convertToShares(uint256 assets) public view virtual override returns (uint256) {
return _convertToShares(assets, true);
}

function _msgSenderForBorrow() internal view returns (address) {
address sender = msg.sender;
bool controllerEnabled;

if (sender == address(evc)) {
(sender, controllerEnabled) = evc.getCurrentOnBehalfOfAccount(address(this));
} else {
controllerEnabled = evc.isControllerEnabled(sender, address(this));
}

if (!controllerEnabled) {
revert ControllerDisabled();
}

return sender;
}

function createVaultSnapshot() internal {
if (snapshot.length == 0) {
snapshot = doCreateVaultSnapshot();
}
}

function doCreateVaultSnapshot() internal virtual returns (bytes memory) {
(uint256 currentTotalBorrowed,) = _accrueInterest();

return abi.encode(_convertToAssets(totalSupply(), false), currentTotalBorrowed);
}

function requireAccountAndVaultStatusCheck(address account) internal {
if (account == address(0)) {
evc.requireVaultStatusCheck();
} else {
evc.requireAccountAndVaultStatusCheck(account);
}
}

function getAccountOwner(address account) internal view returns (address owner) {
try evc.getAccountOwner(account) returns (address _owner) {
owner = _owner;
} catch {
owner = account;
}
}

function _increaseOwed(address account, uint256 assets) internal virtual {
owed[account] = _debtOf(account) + assets;
totalBorrowed += assets;
userInterestAccumulator[account] = interestAccumulator;
}

function _decreaseOwed(address account, uint256 assets) internal virtual {
owed[account] = _debtOf(account) - assets;
totalBorrowed -= assets;
userInterestAccumulator[account] = interestAccumulator;
}

function _debtOf(address account) internal view virtual returns (uint256) {
uint256 debt = owed[account];

if (debt == 0) return 0;

(, uint256 currentInterestAccumulator, bool shouldUpdate) = _accrueInterestCalculate();
if(!shouldUpdate){
return debt;
}
return (debt * currentInterestAccumulator) / userInterestAccumulator[account];
}

function debtOf(address account) public view virtual returns (uint256) {
return _debtOf(account);
}

function _accrueInterest() internal virtual returns (uint256, uint256) {
(uint256 currentTotalBorrowed, uint256 currentInterestAccumulator, bool shouldUpdate) =
_accrueInterestCalculate();

if (shouldUpdate) {
totalBorrowed = currentTotalBorrowed;
interestAccumulator = currentInterestAccumulator;
lastInterestUpdate = block.timestamp;
}

return (currentTotalBorrowed, currentInterestAccumulator);
}

function _accrueInterestCalculate() internal view virtual returns (uint256, uint256, bool) {
uint256 timeElapsed = block.timestamp - lastInterestUpdate;
uint256 oldTotalBorrowed = totalBorrowed;
uint256 oldInterestAccumulator = interestAccumulator;

if (timeElapsed == 0) {
return (oldTotalBorrowed, oldInterestAccumulator, false);
}

uint256 newInterestAccumulator = (
FixedPointMathLib.rpow(uint256(int256(computeInterestRate()) + int256(ONE)), timeElapsed, ONE)
* oldInterestAccumulator
) / ONE;

uint256 newTotalBorrowed = (oldTotalBorrowed * newInterestAccumulator) / oldInterestAccumulator;

return (newTotalBorrowed, newInterestAccumulator, true);
}

function computeInterestRate() internal view virtual returns (int96) {
return int96(int256(uint256((1e27 * interestRate) / 100) / (86400 * 365)));
}

function isControllerEnabled(address account, address vault) internal view returns (bool) {
return evc.isControllerEnabled(account, vault);
}

// IWorkshopVault
function borrow(uint256 assets, address receiver) external {}
function repay(uint256 assets, address receiver) external {}
function pullDebt(address from, uint256 assets) external returns (bool) {}
function liquidate(address violator, address collateral) external {}
function borrow(uint256 assets, address receiver) external callThroughEVC {
address msgSender = _msgSenderForBorrow();

createVaultSnapshot();

require(assets != 0, "ZERO_ASSETS");

// users might input an EVC subaccount, in which case we want to send tokens to the owner
receiver = getAccountOwner(receiver);

_increaseOwed(msgSender, assets);

emit Borrow(msgSender, receiver, assets);

IERC20(asset()).transfer(receiver, assets);

_totalAssets -= assets;

requireAccountAndVaultStatusCheck(msgSender);
}

function repay(uint256 assets, address receiver) external callThroughEVC {
address msgSender = _msgSender();

if (!isControllerEnabled(receiver, address(this))) {
revert ControllerDisabled();
}

createVaultSnapshot();

require(assets != 0, "ZERO_ASSETS");

IERC20(asset()).transferFrom(msgSender, address(this), assets);

_totalAssets += assets;

_decreaseOwed(receiver, assets);

emit Repay(msgSender, receiver, assets);

requireAccountAndVaultStatusCheck(address(0));
}

function pullDebt(address from, uint256 assets) external callThroughEVC returns (bool) {
address msgSender = _msgSenderForBorrow();

if (!isControllerEnabled(from, address(this))) {
revert ControllerDisabled();
}

createVaultSnapshot();

require(assets != 0, "ZERO_AMOUNT");
require(msgSender != from, "SELF_DEBT_PULL");

_decreaseOwed(from, assets);
_increaseOwed(msgSender, assets);

emit Repay(msgSender, from, assets);
emit Borrow(msgSender, msgSender, assets);

requireAccountAndVaultStatusCheck(msgSender);

return true;
}

/// Extremely naive liquidation implementation
function liquidate(address violator, address collateral) external callThroughEVC {
address msgSender = _msgSenderForBorrow();
if (balanceOf(violator) >= debtOf(violator) ) {
revert NoLiquidationOpportunity();
}

doCreateVaultSnapshot();

uint256 seizeShares = debtOf(violator) ;

_decreaseOwed(violator, seizeShares);
_increaseOwed(msgSender, seizeShares);

emit Repay(msgSender, violator, seizeShares);
emit Borrow(msgSender, msgSender, seizeShares);

_transfer(violator, msgSender, seizeShares);

emit Transfer(violator, msgSender, seizeShares);
}
}
54 changes: 52 additions & 2 deletions src/workshop_3/PositionManager.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,57 @@
// SPDX-License-Identifier: GPL-2.0-or-later

pragma solidity ^0.8.19;

import "evc-playground/vaults/VaultRegularBorrowable.sol";
import "evc-playground/vaults/VaultSimple.sol";
import "solmate/utils/SafeTransferLib.sol";
import "forge-std/console2.sol";

contract PositionManager {
using SafeTransferLib for ERC20;

IEVC public immutable evc;
mapping(address => bool) public vaults;
mapping(address => uint256) public lastRebalanceTimestamp;

constructor(IEVC _evc, address[] memory _vaults) {
evc = _evc;
for (uint256 i = 0; i < _vaults.length; i += 1) {
vaults[_vaults[i]] = true;
}
}

function rebalance(address _onBehalfOfAccount, address _currentVault, address _newVault) public {
require(
lastRebalanceTimestamp[_onBehalfOfAccount] == 0
|| block.timestamp >= lastRebalanceTimestamp[_onBehalfOfAccount] + 1 days,
"Rebalance can only be performed once a day"
);
require(vaults[_newVault], "Not allowed vault");
require(
VaultRegularBorrowable(_newVault).getInterestRate()
> VaultRegularBorrowable(_currentVault).getInterestRate(),
"Cannot rebalance to a lowest rate vault"
);

ERC20 asset = ERC4626(_currentVault).asset();
uint256 assets = VaultRegularBorrowable(_currentVault).maxWithdraw(_onBehalfOfAccount);

// if there's anything to withdraw, withdraw it to this contract
evc.call(
_currentVault,
_onBehalfOfAccount,
0,
abi.encodeWithSelector(VaultSimple.withdraw.selector, assets, address(this), _onBehalfOfAccount)
);

// transfer 1% of the withdrawn assets as a tip to the msg.sender
asset.safeTransfer(msg.sender, assets / 100);

// rebalance the rest on behalf of account
asset.approve(_newVault, ERC20(asset).balanceOf(address(this)));
VaultRegularBorrowable(_newVault).deposit(ERC20(asset).balanceOf(address(this)), _onBehalfOfAccount);

contract PositionManager {}
// Update the last rebalance timestamp
lastRebalanceTimestamp[_onBehalfOfAccount] = block.timestamp;
}
}
Loading