diff --git a/src/escrows/MakerEscrow.sol b/src/escrows/MakerEscrow.sol new file mode 100644 index 00000000..913132b2 --- /dev/null +++ b/src/escrows/MakerEscrow.sol @@ -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; + } + } + +} diff --git a/test/escrowForkTests/MakerEscrowFork.t.sol b/test/escrowForkTests/MakerEscrowFork.t.sol new file mode 100644 index 00000000..89740f5c --- /dev/null +++ b/test/escrowForkTests/MakerEscrowFork.t.sol @@ -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()); + } + +} diff --git a/test/marketForkTests/MakerMarketForkTest.t.sol b/test/marketForkTests/MakerMarketForkTest.t.sol new file mode 100644 index 00000000..098b805b --- /dev/null +++ b/test/marketForkTests/MakerMarketForkTest.t.sol @@ -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); + } +} diff --git a/test/marketForkTests/MarketBaseForkTest.sol b/test/marketForkTests/MarketBaseForkTest.sol index 8dce24eb..112937b1 100644 --- a/test/marketForkTests/MarketBaseForkTest.sol +++ b/test/marketForkTests/MarketBaseForkTest.sol @@ -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 { diff --git a/test/marketForkTests/MarketForkTest.sol b/test/marketForkTests/MarketForkTest.sol index 60aa7966..b1368c68 100644 --- a/test/marketForkTests/MarketForkTest.sol +++ b/test/marketForkTests/MarketForkTest.sol @@ -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;