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

Cooler v2 #46

Open
wants to merge 97 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
97 commits
Select commit Hold shift + click to select a range
8837b70
sync with olympus-v3
frontier159 Dec 5, 2024
c857395
proxy FORK_TEST_RPC_URL
frontier159 Dec 5, 2024
6ff2bdd
fix CI
frontier159 Dec 5, 2024
01f63b6
feat: draft cooler v2
frontier159 Oct 25, 2024
da1fa26
feat: added some notes
frontier159 Oct 25, 2024
66280a4
feat: start adding tests
frontier159 Oct 28, 2024
6cc1997
feat: split out delegations into new module
frontier159 Nov 20, 2024
d2a17cf
feat: rework DLGTE, add clones for escrow
frontier159 Nov 21, 2024
e3d0260
feat: added borrow/repay tests
frontier159 Nov 22, 2024
21b9aac
fix: review feedback
frontier159 Nov 27, 2024
fc9f1a3
chore: lint
frontier159 Nov 27, 2024
62eaa04
add composite functions for UX
frontier159 Dec 5, 2024
b00743c
start composite tests
frontier159 Dec 5, 2024
2e6e071
update build post merge
frontier159 Dec 6, 2024
c6a4f4f
lint
frontier159 Dec 6, 2024
9c360a8
feat: add composite tests, fixes
frontier159 Dec 11, 2024
6e12620
fix: remove CHREG dep
frontier159 Dec 11, 2024
ca3f007
don't allow origination LTV to be reduced
frontier159 Dec 23, 2024
5c16fbf
bump forge-std version
frontier159 Jan 7, 2025
d05c91c
ltv oracle
frontier159 Jan 20, 2025
42ce7ee
update ltv oracle tests
frontier159 Jan 21, 2025
e0707de
update tests to have realistic OLTV
frontier159 Jan 22, 2025
e575265
initial implementation of adding 3rd party authorization
frontier159 Jan 22, 2025
d74d571
Merge pull request #443 from OlympusDAO/cooler-v2-ltv-oracle
frontier159 Jan 22, 2025
a2c3cd8
completed tests
frontier159 Jan 22, 2025
49a8ad2
Merge pull request #444 from OlympusDAO/cooler-v2-operator
frontier159 Jan 23, 2025
0162fdc
separated out the treasury borrow/repay into a new module
frontier159 Jan 23, 2025
8405277
roles update for trsry borrower, test to change debt token on the fly
frontier159 Jan 29, 2025
2842651
Merge pull request #445 from OlympusDAO/cooler-v2-trsry-borrower
frontier159 Jan 29, 2025
a5ce630
nit: rename wei to wad
frontier159 Jan 29, 2025
3364757
added liquidation incentive
frontier159 Jan 29, 2025
24c7730
liquidations tests
frontier159 Jan 29, 2025
34031b7
cooler liquidation tests
frontier159 Feb 3, 2025
5202871
completed tests for liquidations
frontier159 Feb 4, 2025
537ac6f
Merge pull request #446 from OlympusDAO/cooler-v2-incentives
frontier159 Feb 4, 2025
ef216fd
missing test update
frontier159 Feb 4, 2025
a6ab8ac
cleanup
frontier159 Feb 4, 2025
b91ab4b
First pass at function for enable/disable functionality
0xJem Feb 4, 2025
61ac6f7
Add documentation
0xJem Feb 4, 2025
6d57c21
Make implementation-specific functions optional
0xJem Feb 4, 2025
05649c0
update the interest rate to be in WAD
frontier159 Feb 4, 2025
46f3a45
Rename Enableable -> PolicyEnabler and change directories
0xJem Feb 5, 2025
e76051c
Add role constants
0xJem Feb 5, 2025
e173fb1
Allow emergency or admin access
0xJem Feb 5, 2025
6582415
Rename modifiers
0xJem Feb 6, 2025
3ec98ae
Add tests
0xJem Feb 6, 2025
7221437
chore: tidy comments
frontier159 Feb 10, 2025
ccc40dd
Merge remote-tracking branch 'origin/bophades/cooler-v2' into cooler-v2
0xJem Feb 10, 2025
2b85dcf
Amend install script to use forge-std 1.9.5
0xJem Feb 10, 2025
e3e452c
Fixes to Cooler/Clearinghouse tests due to MockStaking/MockGohm changes
0xJem Feb 10, 2025
5592522
Fix Quabi-dependent tests caused by changes to optimizer runs and com…
0xJem Feb 10, 2025
132f592
Restore old MockStaking, rename new MockStaking to MockStakingReal
0xJem Feb 10, 2025
6b7719a
Fixes failing EmissionManager test by reverting gOHM index change
0xJem Feb 10, 2025
35f3a4a
pOLY fuzz test fix
0xJem Feb 10, 2025
8a8b1ff
chore: linting
0xJem Feb 10, 2025
0adb257
Revert git submodule change
0xJem Feb 10, 2025
3505d63
Typo
0xJem Feb 10, 2025
95cddab
Document new role
0xJem Feb 10, 2025
b088e9f
ROLES
0xJem Feb 10, 2025
7b6b296
Merge branch 'policy-improvements' into cooler-v2-policy-enabler
0xJem Feb 10, 2025
434f547
Add PolicyEnabler to CoolerTreasuryBorrower, consolidate on "admin" r…
0xJem Feb 10, 2025
dfd99ea
Shift CoolerLtvOracle to use PolicyEnabler. Updates tests.
0xJem Feb 10, 2025
6b47e35
Update ROLES
0xJem Feb 10, 2025
ff6a665
Add isAdmin() and isEmergency() to PolicyEnabler
0xJem Feb 10, 2025
cedeb02
Add PolicyEnabler to MonoCooler
0xJem Feb 10, 2025
f3c93ef
borrow and liquidation actions fail if the policy is not enabled. Gat…
0xJem Feb 10, 2025
49f2559
Trying to reduce contract size
0xJem Feb 10, 2025
20593b1
Shift interfaces to use included IERC20 interface to remove external …
0xJem Feb 11, 2025
00090cc
Use ERC20 for internal variables, add public getters. Strangely reduc…
0xJem Feb 11, 2025
916d392
Fix compile error from previous commit
0xJem Feb 11, 2025
e397f09
Return IERC20 in ICoolerLtvOracle instead of address
0xJem Feb 11, 2025
c3333e4
Shift structs/errors/events from DLGTEv1 into interface
0xJem Feb 11, 2025
b359ff2
Relative imports in interfaces to avoid remappings hell
0xJem Feb 11, 2025
70bd83b
Merge branch 'cooler-v2' into cooler-v2-policy-enabler
0xJem Feb 11, 2025
708e365
Add remaining tests
0xJem Feb 11, 2025
3df1eda
Extract functions
0xJem Feb 11, 2025
45026b5
Reduce bytecode size of MonoCooler by tweaking optimiser
0xJem Feb 11, 2025
78476a5
Use SHA instead of version tag
0xJem Feb 11, 2025
4ebc3f1
Merge branch 'cooler-v2' into cooler-v2-policy-enabler
0xJem Feb 11, 2025
34bf6ea
Fix issues with code coverage
0xJem Feb 11, 2025
77cdc4b
Disable lcov comment for now
0xJem Feb 11, 2025
b730dd0
Merge branch 'cooler-v2' into cooler-v2-policy-enabler
0xJem Feb 11, 2025
ec1ca21
Extract admin/emergency checks into PolicyAdmin mix-in
0xJem Feb 11, 2025
ec596e1
Update MonoCooler to use PolicyAdmin
0xJem Feb 11, 2025
ff2d404
Add convenience modifiers for admin and emergency roles
0xJem Feb 11, 2025
60fc082
Shift cooler contracts to use PolicyAdmin modifiers
0xJem Feb 11, 2025
dcb25b0
More PolicyAdmin changes
0xJem Feb 11, 2025
da24bc8
Merge pull request #47 from OlympusDAO/cooler-v2-policy-enabler
0xJem Feb 11, 2025
9c066a8
update timestamp to uint40 to future proof
frontier159 Feb 13, 2025
7a599fd
ltv oracle: update timestamp to uint40 to future proof
frontier159 Feb 14, 2025
22b05ad
Merge branch 'develop' into cooler-v2
0xJem Feb 17, 2025
2412de7
Apply foundry 1.0 migration fixes
0xJem Feb 17, 2025
8b8c8ea
fix: UnauthorizedOnBehalfOf typo
frontier159 Feb 17, 2025
1d8adcf
fix: cap liquidation incentive to user collateral
frontier159 Feb 17, 2025
5aae2a7
fix: accountDelegationsList paging
frontier159 Feb 17, 2025
6b702f0
chore: lint
frontier159 Feb 17, 2025
a47670c
chore: pull DLGTE interface out
frontier159 Feb 17, 2025
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
Prev Previous commit
Next Next commit
cooler liquidation tests
  • Loading branch information
frontier159 committed Feb 3, 2025
commit 34031b75f2bf4e97e1b7d9bdb303f032ab59f17d
24 changes: 6 additions & 18 deletions src/external/cooler/DelegateEscrowFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,8 @@ import {DelegateEscrow} from "src/external/cooler/DelegateEscrow.sol";
contract DelegateEscrowFactory {
using ClonesWithImmutableArgs for address;

// --- ERRORS ----------------------------------------------------

error NotFromFactory();

// --- EVENTS ----------------------------------------------------

/// @notice A caller has created a new escrow for a delegate
event DelegateEscrowCreated(
address indexed caller,
Expand All @@ -36,8 +32,6 @@ contract DelegateEscrowFactory {
int256 delegationAmountDelta
);

// -- STATE VARIABLES --------------------------------------------

/// @notice Reference implementation (deployed on creation to clone from).
DelegateEscrow public immutable escrowImplementation;

Expand All @@ -47,14 +41,10 @@ contract DelegateEscrowFactory {
/// @notice Mapping to query escrows for a given delegate.
mapping(address => DelegateEscrow) public escrowFor;

// --- INITIALIZATION --------------------------------------------

constructor(address gohm_) {
escrowImplementation = new DelegateEscrow(gohm_);
}

// --- DEPLOY NEW COOLERS ----------------------------------------

/// @notice creates a new escrow contract for a delegate.
function create(address delegate) external returns (DelegateEscrow escrow) {
escrow = escrowFor[delegate];
Expand All @@ -76,14 +66,6 @@ contract DelegateEscrowFactory {
}
}

// --- EMIT EVENTS -----------------------------------------------

/// @notice Ensure that the called is a Cooler.
modifier onlyFromFactory() {
if (!created[msg.sender]) revert NotFromFactory();
_;
}

/// @notice Emit a global event when a new loan request is created.
function logDelegate(
address caller,
Expand All @@ -92,4 +74,10 @@ contract DelegateEscrowFactory {
) external onlyFromFactory {
emit Delegate(msg.sender, caller, onBehalfOf, delegationAmountDelta);
}

/// @notice Ensure that the called is a Cooler.
modifier onlyFromFactory() {
if (!created[msg.sender]) revert NotFromFactory();
_;
}
}
50 changes: 25 additions & 25 deletions src/policies/cooler/MonoCooler.sol
Original file line number Diff line number Diff line change
Expand Up @@ -503,25 +503,21 @@ contract MonoCooler is IMonoCooler, Policy, RolesConsumer {
// LIQUIDATIONS //
//============================================================================================//

// @todo incentivise liquidations
/*
Frontier:
> I wasn't sure how we want to incentivise liquidations. Heart based model with increasing reward doesn't work here imo since we can't easily know the start time of when it first became unhealthy.

Anon:
> First thought that comes to mind is have interest keep accruing but give the keeper the delta above liq point? kind of an already there gda mechanism

It means the liquidation won't happen until that accrued interest pays for gas++, but probably not a problem if it's a few hours/days after the fact. It’s a softer liquidation mechanism but that’s more in the nature of expiry time to debt threshold, ie you could top up or repay before liquidation bot actually liquidates
*/

/// @inheritdoc IMonoCooler
function batchLiquidate(
address[] calldata accounts,
DLGTEv1.DelegationRequest[][] calldata delegationRequests
) external override returns (uint128 totalCollateralClaimed, uint128 totalDebtWiped) {
) external override returns (
uint128 totalCollateralClaimed,
uint128 totalDebtWiped,
uint128 totalLiquidationIncentive
) {
if (liquidationsPaused) revert Paused();

uint256 totalLiquidationIncentive;
// Delegation requests can be left empty, otherwise it needs to be the same length as the
// `accounts`.
if (delegationRequests.length > 0 && delegationRequests.length != accounts.length) revert InvalidDelegationRequests();

LiquidationStatus memory status;
GlobalStateCache memory gState = _globalStateRW();
address account;
Expand All @@ -532,17 +528,21 @@ It means the liquidation won't happen until that accrued interest pays for gas++

// Skip if this account is still under the maxLTV
if (status.exceededLiquidationLtv) {
emit Liquidated(account, status.collateral, status.currentDebt);

// Apply any undelegation requests
DLGTEv1.DelegationRequest[] calldata dreqs = delegationRequests[i];
if (dreqs.length > 1) {
// Note: More collateral may be undelegated than required for the liquidation here.
// But this is assumed ok - the liquidated user will need to re-apply the delegations again.
(uint256 appliedDelegations, ) = DLGTE.applyDelegations(account, dreqs);

// For liquidations, only allow undelegation requests
if (appliedDelegations > 0) revert InvalidDelegationRequests();
emit Liquidated(msg.sender, account, status.collateral, status.currentDebt, status.currentIncentive);

// Apply any undelegation requests.
// Can be left empty, otherwise needs to be the same length as the
// `accounts`.
if (delegationRequests.length > 0) {
DLGTEv1.DelegationRequest[] calldata dreqs = delegationRequests[i];
if (dreqs.length > 0) {
// Note: More collateral may be undelegated than required for the liquidation here.
// But this is assumed ok - the liquidated user will need to re-apply the delegations again.
(uint256 appliedDelegations, ) = DLGTE.applyDelegations(account, dreqs);

// Only allow undelegation requests
if (appliedDelegations > 0) revert InvalidDelegationRequests();
}
}

// Withdraw the undelegated gOHM
Expand All @@ -560,7 +560,7 @@ It means the liquidation won't happen until that accrued interest pays for gas++
// burn the gOHM collateral and update the total state.
if (totalCollateralClaimed > 0) {
// Unstake and burn gOHM holdings.
uint256 gOhmToBurn = totalCollateralClaimed - totalLiquidationIncentive;
uint128 gOhmToBurn = totalCollateralClaimed - totalLiquidationIncentive;
collateralToken.safeApprove(address(staking), gOhmToBurn);

MINTR.burnOhm(
Expand Down
8 changes: 6 additions & 2 deletions src/policies/interfaces/IMonoCooler.sol
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ interface IMonoCooler {
address indexed onBehalfOf,
uint128 repayAmount
);
event Liquidated(address indexed account, uint128 collateralSeized, uint128 debtWiped);
event Liquidated(address indexed caller, address indexed account, uint128 collateralSeized, uint128 debtWiped, uint128 incentives);
event AuthorizationSet(address indexed caller, address indexed account, address indexed authorized, uint96 authorizationDeadline);

/// @notice The record of an individual account's collateral and debt data
Expand Down Expand Up @@ -376,7 +376,11 @@ interface IMonoCooler {
function batchLiquidate(
address[] calldata accounts,
DLGTEv1.DelegationRequest[][] calldata delegationRequests
) external returns (uint128 totalCollateralClaimed, uint128 totalDebtWiped);
) external returns (
uint128 totalCollateralClaimed,
uint128 totalDebtWiped,
uint128 totalLiquidationIncentive
);

/**
* @notice If an account becomes unhealthy and has many delegations such that liquidation can't be
Expand Down
172 changes: 172 additions & 0 deletions src/test/external/cooler/DelegateEscrowFactory.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// SPDX-License-Identifier: MAGPL-3.0-only
pragma solidity 0.8.15;

import {Test} from "forge-std/Test.sol";
import {MockGohm} from "test/mocks/MockGohm.sol";

import {DelegateEscrowFactory} from "src/external/cooler/DelegateEscrowFactory.sol";
import {DelegateEscrow} from "src/external/cooler/DelegateEscrow.sol";
import {IVotes} from "openzeppelin/governance/utils/IVotes.sol";

contract DelegateEscrowFactoryTestBase is Test {

address public immutable ALICE = makeAddr("ALICE");
address public immutable BOB = makeAddr("BOB");
address public immutable CHARLIE = makeAddr("CHARLIE");

MockGohm internal gohm;
DelegateEscrowFactory escrowFactory;

event DelegateEscrowCreated(
address indexed caller,
address indexed delegate,
address indexed escrow
);

event Delegate(
address indexed escrow,
address indexed caller,
address indexed onBehalfOf,
int256 delegationAmountDelta
);

function setUp() public virtual {
gohm = new MockGohm("gOHM", "gOHM", 18);
escrowFactory = new DelegateEscrowFactory(address(gohm));
}
}

contract DelegateEscrowFactoryTest is DelegateEscrowFactoryTestBase {
function test_create_new() public {
vm.startPrank(ALICE);
address expectedEscrow = 0x8d2C17FAd02B7bb64139109c6533b7C2b9CADb81;
vm.assertEq(address(escrowFactory.escrowFor(BOB)), address(0));
vm.assertEq(escrowFactory.created(expectedEscrow), false);

vm.expectEmit(address(escrowFactory));
emit DelegateEscrowCreated(ALICE, BOB, expectedEscrow);
DelegateEscrow escrow = escrowFactory.create(BOB);
assertEq(address(escrow), expectedEscrow);

vm.assertEq(address(escrowFactory.escrowFor(BOB)), expectedEscrow);
vm.assertEq(escrowFactory.created(expectedEscrow), true);
}

function test_create_existing() public {
vm.startPrank(ALICE);
address expectedEscrow = 0x8d2C17FAd02B7bb64139109c6533b7C2b9CADb81;
DelegateEscrow escrow = escrowFactory.create(BOB);
assertEq(address(escrow), expectedEscrow);
escrow = escrowFactory.create(BOB);
assertEq(address(escrow), expectedEscrow);

vm.assertEq(address(escrowFactory.escrowFor(BOB)), expectedEscrow);
vm.assertEq(escrowFactory.created(expectedEscrow), true);
}

function test_access_logDelegate() public {
vm.startPrank(ALICE);
vm.expectRevert(abi.encodeWithSelector(DelegateEscrowFactory.NotFromFactory.selector));
escrowFactory.logDelegate(ALICE, ALICE, 123);
}

function test_logDelegate_failFactory() public {
vm.startPrank(ALICE);
vm.expectRevert(abi.encodeWithSelector(DelegateEscrowFactory.NotFromFactory.selector));
escrowFactory.logDelegate(ALICE, ALICE, 123);
}

function test_logDelegate_success() public {
vm.startPrank(ALICE);
DelegateEscrow escrow = escrowFactory.create(ALICE);
vm.startPrank(address(escrow));
vm.expectEmit(address(escrowFactory));
emit Delegate(address(escrow), ALICE, BOB, 123);
escrowFactory.logDelegate(ALICE, BOB, 123);
}
}

contract DelegateEscrowImplTest is DelegateEscrowFactoryTestBase {
DelegateEscrow internal aliceEscrow;

address internal immutable CALLER1 = makeAddr("CALLER1");
address internal immutable CALLER2 = makeAddr("CALLER2");

function delegate(address caller, uint256 amount, address onBehalfOf) internal returns (uint256) {
vm.startPrank(caller);
deal(address(gohm), caller, amount);
gohm.approve(address(aliceEscrow), amount);

vm.expectEmit(address(escrowFactory));
emit Delegate(address(aliceEscrow), caller, onBehalfOf, int256(amount));
return aliceEscrow.delegate(onBehalfOf, amount);
}

function rescind(address caller, address onBehalfOf, uint256 amount) internal returns (uint256 remaining) {
vm.startPrank(caller);
uint256 balBefore = gohm.balanceOf(caller);
vm.expectEmit(address(escrowFactory));
emit Delegate(address(aliceEscrow), caller, onBehalfOf, -int256(amount));
remaining = aliceEscrow.rescindDelegation(onBehalfOf, amount);
assertEq(gohm.balanceOf(caller)-balBefore, amount);
return remaining;
}

function setUp() public override {
DelegateEscrowFactoryTestBase.setUp();
aliceEscrow = escrowFactory.create(ALICE);
}

function test_constructor() public view {
assertEq(IVotes(address(gohm)).delegates(address(aliceEscrow)), ALICE);
assertEq(aliceEscrow.delegateAccount(), ALICE);
assertEq(address(aliceEscrow.factory()), address(escrowFactory));
}

function test_delegate() public {
assertEq(delegate(CALLER1, 10e18, ALICE), 10e18);
assertEq(aliceEscrow.delegations(CALLER1, ALICE), 10e18);
assertEq(delegate(CALLER1, 33e18, ALICE), 43e18);
assertEq(aliceEscrow.delegations(CALLER1, ALICE), 43e18);
assertEq(delegate(CALLER1, 10e18, BOB), 10e18);
assertEq(aliceEscrow.delegations(CALLER1, ALICE), 43e18);
assertEq(aliceEscrow.delegations(CALLER1, BOB), 10e18);

assertEq(delegate(CALLER2, 10e18, ALICE), 10e18);
assertEq(delegate(CALLER2, 33e18, ALICE), 43e18);
assertEq(delegate(CALLER2, 10e18, BOB), 10e18);
assertEq(aliceEscrow.delegations(CALLER2, ALICE), 43e18);
assertEq(aliceEscrow.delegations(CALLER2, BOB), 10e18);

assertEq(aliceEscrow.delegations(CALLER1, ALICE), 43e18);
assertEq(aliceEscrow.delegations(CALLER1, BOB), 10e18);
}

function test_rescindDelegation() public {
delegate(CALLER1, 43e18, ALICE);
delegate(CALLER1, 10e18, BOB);
delegate(CALLER2, 43e18, ALICE);
delegate(CALLER2, 10e18, BOB);

vm.startPrank(CALLER1);
vm.expectRevert(abi.encodeWithSelector(DelegateEscrow.ExceededDelegationBalance.selector));
aliceEscrow.rescindDelegation(ALICE, 43e18+1);

assertEq(rescind(CALLER1, ALICE, 5e18), 38e18);
assertEq(aliceEscrow.delegations(CALLER1, ALICE), 38e18);
assertEq(rescind(CALLER1, ALICE, 5e18), 33e18);
assertEq(aliceEscrow.delegations(CALLER1, ALICE), 33e18);
assertEq(rescind(CALLER1, BOB, 5e18), 5e18);
assertEq(aliceEscrow.delegations(CALLER1, BOB), 5e18);

assertEq(rescind(CALLER2, ALICE, 5e18), 38e18);
assertEq(aliceEscrow.delegations(CALLER2, ALICE), 38e18);
assertEq(rescind(CALLER2, ALICE, 5e18), 33e18);
assertEq(aliceEscrow.delegations(CALLER2, ALICE), 33e18);
assertEq(rescind(CALLER2, BOB, 5e18), 5e18);
assertEq(aliceEscrow.delegations(CALLER2, BOB), 5e18);

assertEq(aliceEscrow.delegations(CALLER1, ALICE), 33e18);
assertEq(aliceEscrow.delegations(CALLER1, BOB), 5e18);
}
}
11 changes: 6 additions & 5 deletions src/test/mocks/MockGohm.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,23 @@ pragma solidity ^0.8.0;
import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol";

interface IDelegate {
function delegate(address) external returns (bool);
function delegate(address) external;
function delegates(address) external view returns (address);
}

contract MockGohm is MockERC20, IDelegate {
uint256 public constant index = 10000;
address public delegatee;

mapping(address => address) public override delegates;

constructor(
string memory name_,
string memory symbol_,
uint8 decimals_
) MockERC20(name_, symbol_, decimals_) {}

function delegate(address delegatee_) public returns (bool) {
delegatee = delegatee_;
return true;
function delegate(address delegatee_) public {
delegates[msg.sender] = delegatee_;
}

function balanceFrom(uint256 amount_) public view returns (uint256) {
Expand Down
Loading