Skip to content
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
143 changes: 143 additions & 0 deletions src/escrows/MakerEscrow.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "src/interfaces/IERC20.sol";

/// * @dev Caution: We assume all failed transfers cause reverts and ignore the returned bool.
interface IVoteDelegate {
function lock(uint) external;
function free(uint) external;
function stake(address) external view returns(uint);
function delegate() external view returns(address);
function expiration() external view returns(uint);
}

interface IVoteDelegateFactory {
function isDelegate(address) external view returns(bool);
function delegates(address) external view returns(address);
}

/**
* @title Simple ERC20 Escrow
* @notice Collateral is stored in unique escrow contracts for every user and every market.
* @dev Caution: This is a proxy implementation. Follow proxy pattern best practices
*/
contract MakerEscrow {
address public market;
address public beneficiary;
IVoteDelegate public voteDelegate;
IVoteDelegateFactory public constant voteDelegateFactory = IVoteDelegateFactory(0xD897F108670903D1d6070fcf818f9db3615AF272);
IERC20 public constant iou = IERC20(0xA618E54de493ec29432EbD2CA7f14eFbF6Ac17F7);
IERC20 public constant token = IERC20(0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2);

error OnlyBeneficiary();

/**
* @notice Initialize escrow with a token
* @dev Must be called right after proxy is created
* @param _beneficiary Address of the owner of the escrow
*/
function initialize(IERC20, address _beneficiary) public {
require(market == address(0), "ALREADY INITIALIZED");
market = msg.sender;
beneficiary = _beneficiary;
}

/**
* @notice Transfers the associated ERC20 token to a recipient.
* @param recipient The address to receive payment from the escrow
* @param amount The amount of ERC20 token to be transferred.
*/
function pay(address recipient, uint amount) public {
require(msg.sender == market, "ONLY MARKET");
uint mkrBal = token.balanceOf(address(this));
if(amount > mkrBal){
voteDelegate.free(amount - mkrBal);
}
token.transfer(recipient, amount);
}

/**
* @notice Get the token balance of the escrow
* @return Uint representing the token balance of the escrow
*/
function balance() public view returns (uint) {
return token.balanceOf(address(this)) + iou.balanceOf(address(this));
}

/**
* @notice Function called by market on deposit. Function is empty for this escrow.
* @dev This function should remain callable by anyone to handle direct inbound transfers.
*/
function onDeposit() external {
if(address(voteDelegate) != address(0)){
uint mkrBal = token.balanceOf(address(this));
voteDelegate.lock(mkrBal);
} else if(voteDelegateFactory.isDelegate(beneficiary)){
uint mkrBal = token.balanceOf(address(this));
_setVoteDelegate(
IVoteDelegate(voteDelegateFactory.delegates(beneficiary))
);
voteDelegate.lock(mkrBal);
}
}

/**
* @notice Delegates all voting power to the address `_newDelegate`
* @dev `_newDelegate` is not the address of a `VoteDelegate` contract but the owner of such a contract.
* @param _newDelegate Address of the new delegate to delegate to
*/
function delegateTo(address _newDelegate) external {
if(msg.sender != beneficiary) revert OnlyBeneficiary();
if(address(voteDelegate) != address(0)){
uint stake = voteDelegate.stake(address(this));
voteDelegate.free(stake);
}
uint mkrBal = token.balanceOf(address(this));
_setVoteDelegate(
IVoteDelegate(voteDelegateFactory.delegates(_newDelegate))
);
voteDelegate.lock(mkrBal);
}

function _setVoteDelegate(IVoteDelegate newVoteDelegate) internal {
voteDelegate = newVoteDelegate;
iou.approve(address(newVoteDelegate), type(uint).max);
token.approve(address(newVoteDelegate), type(uint).max);
}

/**
* @notice Undelegates from the current delegate
*/
function undelegate() external {
if(msg.sender != beneficiary) revert OnlyBeneficiary();
if(address(voteDelegate) != address(0)){
uint stake = voteDelegate.stake(address(this));
voteDelegate.free(stake);
voteDelegate = IVoteDelegate(address(0));
}
}

/**
* @notice Get the owner of the `voteDelegate` contract that is being delegated to.
*/
function delegate() external view returns(address){
if(address(voteDelegate) != address(0)){
return voteDelegate.delegate();
} else {
return address(0);
}
}

/**
* @notice Return the expiry of the delegation contract, at which point the contract will have to be redelegated.
*/
function expiration() external view returns(uint){
if(address(voteDelegate) != address(0)){
return voteDelegate.expiration();
} else {
return 0;
}
}

}
118 changes: 118 additions & 0 deletions test/escrowForkTests/MakerEscrowFork.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
pragma solidity ^0.8.20;

import "test/escrowForkTests/BaseEscrowTest.t.sol";
import {MakerEscrow, IVoteDelegate, IVoteDelegateFactory} from "src/escrows/MakerEscrow.sol";

contract MakerEscrowFork is BaseEscrowTest {

IERC20 maker = IERC20(0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2);
MakerEscrow escrowImplementation;
IVoteDelegateFactory public constant voteDelegateFactory = IVoteDelegateFactory(0xD897F108670903D1d6070fcf818f9db3615AF272);
address delegate = 0xE5a7023f78c3c0b7B098e8f4aCE7031B3D9aFBaB;

function setUp() public {
string memory url = vm.rpcUrl("mainnet");
vm.createSelectFork(url);

escrowImplementation = new MakerEscrow();
initialize(address(escrowImplementation), address(maker));
}

function test_delegateTo() public {
deal(address(maker), address(escrowImplementation), 1 ether);
assertEq(escrowImplementation.delegate(), address(0));
vm.prank(beneficiary);
escrowImplementation.delegateTo(delegate);
assertEq(address(escrowImplementation.voteDelegate()), voteDelegateFactory.delegates(delegate));
assertEq(escrowImplementation.delegate(), delegate);
assertEq(escrowImplementation.iou().balanceOf(address(escrowImplementation)), 1 ether);
assertEq(escrowImplementation.balance(), 1 ether);
}

function test_pay_payAfterDelegateTo() public {
deal(address(maker), address(escrowImplementation), 1 ether);
assertEq(escrowImplementation.delegate(), address(0));
vm.prank(beneficiary);
escrowImplementation.delegateTo(delegate);
vm.roll(block.number + 1); //Will revert if chief is interacted with twice by same address in same block
assertEq(escrowImplementation.delegate(), delegate);
vm.prank(market);
escrowImplementation.pay(beneficiary, 1 ether);
assertEq(maker.balanceOf(beneficiary), 1 ether);
assertEq(escrowImplementation.balance(), 0);
}

function test_delegateTo_afterPayHalf() public {
deal(address(maker), address(escrowImplementation), 1 ether);
vm.prank(market);
escrowImplementation.pay(beneficiary, 1 ether / 2);
assertEq(maker.balanceOf(beneficiary), 1 ether / 2);
assertEq(escrowImplementation.balance(), 1 ether / 2);

assertEq(escrowImplementation.delegate(), address(0));
vm.prank(beneficiary);
escrowImplementation.delegateTo(delegate);
assertEq(escrowImplementation.delegate(), delegate);
assertEq(escrowImplementation.iou().balanceOf(address(escrowImplementation)), 1 ether / 2);
assertEq(escrowImplementation.balance(), 1 ether / 2);
}

function test_onDepositDelegateCorrectly() public {
MakerEscrow freshEscrow = new MakerEscrow();
deal(address(maker), address(freshEscrow), 1 ether);
vm.prank(market);
freshEscrow.initialize(maker, delegate);
assertTrue(voteDelegateFactory.isDelegate(freshEscrow.beneficiary()), "`delegate` is not a Delegate");
uint balBefore = freshEscrow.balance();
freshEscrow.onDeposit();
assertEq(freshEscrow.market(), market);
assertEq(freshEscrow.beneficiary(), delegate);
assertEq(address(freshEscrow.token()), address(maker));
assertEq(freshEscrow.delegate(), freshEscrow.beneficiary(), "Delegate not set");
assertEq(balBefore, freshEscrow.balance(), "Balance after");
}

function test_onDeposit_DelegateCorrectlyAfterExpiry() public {
MakerEscrow freshEscrow = new MakerEscrow();
deal(address(maker), address(freshEscrow), 1 ether);
vm.prank(market);
freshEscrow.initialize(maker, delegate);
assertTrue(voteDelegateFactory.isDelegate(freshEscrow.beneficiary()), "`delegate` is not a Delegate");
uint balBefore = freshEscrow.balance();
freshEscrow.onDeposit();
assertEq(freshEscrow.market(), market);
assertEq(freshEscrow.beneficiary(), delegate);
assertEq(address(freshEscrow.token()), address(maker));
assertEq(freshEscrow.delegate(), freshEscrow.beneficiary(), "Delegate not set");
assertEq(balBefore, freshEscrow.balance(), "Balance after");
}

function test_undelegate() public {
deal(address(maker), address(escrowImplementation), 1 ether);
assertEq(escrowImplementation.delegate(), address(0));
vm.prank(beneficiary);
escrowImplementation.delegateTo(delegate);
assertEq(escrowImplementation.delegate(), delegate);

vm.roll(block.number + 1); //Will revert if chief is interacted with twice by same address in same block
vm.prank(beneficiary);
escrowImplementation.undelegate();
assertEq(escrowImplementation.delegate(), address(0));
assertEq(maker.balanceOf(address(escrowImplementation)), 1 ether);
}

function test_delegateTo_failsWhenCalledByNonBeneficiary() public {
vm.expectRevert();
vm.prank(holder);
escrowImplementation.delegateTo(holder);
}

function test_expiration() public {
assertEq(escrowImplementation.expiration(), 0);
vm.prank(beneficiary);
escrowImplementation.delegateTo(delegate);
IVoteDelegate voteDelegate = escrowImplementation.voteDelegate();
assertEq(escrowImplementation.expiration(), voteDelegate.expiration());
}

}
21 changes: 21 additions & 0 deletions test/marketForkTests/MakerMarketForkTest.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "forge-std/Test.sol";
import "./MarketBaseForkTest.sol";
import "src/escrows/MakerEscrow.sol";

contract MakerMarketForkTest is MarketBaseForkTest {

function setUp() public {
//This will fail if there's no mainnet variable in foundry.toml
string memory url = vm.rpcUrl("mainnet");
vm.createSelectFork(url);
//For non-deployed markets, instantiate market and feed after fork and use new contract addresses
MakerEscrow escrow = new MakerEscrow();
IERC20 maker = IERC20(0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2);
Market market = new Market(gov, lender, pauseGuardian, address(escrow), IDolaBorrowingRights(address(dbr)), maker, IOracle(address(oracle)), 7500, 5000, 1000, true);
address feedAddr = 0xec1D1B3b0443256cc3860e24a46F108e699484Aa ;
_advancedInit(address(market), feedAddr, false);
}
}
1 change: 0 additions & 1 deletion test/marketForkTests/MarketBaseForkTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ abstract contract MarketBaseForkTest is MarketForkTest {
"Only pause guardian or governance can pause";
address lender = 0x2b34548b865ad66A2B046cb82e59eE43F75B90fd;
bool approximateBalance;

BorrowContract borrowContract;

function _baseInit(address _market, address _feed) public {
Expand Down
3 changes: 0 additions & 3 deletions test/marketForkTests/MarketForkTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,6 @@ contract MarketForkTest is Test, ConfigAddr {
address user2 = address(0x70);
address replenisher = address(0x71);
address collatHolder = address(0xD292b72e5C787f9F7E092aB7802aDDF76930981F);
// address gov = address(0x926dF14a23BE491164dCF93f4c468A50ef659D5B);
//address chair = address(0x8F97cCA30Dbe80e7a8B462F1dD1a51C32accDfC8);
//address pauseGuardian = address(0xE3eD95e130ad9E15643f5A5f232a3daE980784cd);

//ERC-20s
IMintable DOLA;
Expand Down