From 8f44d6d7cd7e85a4420087b155dac0b371fb4935 Mon Sep 17 00:00:00 2001 From: Daniel Von Fange Date: Fri, 1 Nov 2024 14:01:59 -0400 Subject: [PATCH] Change to mostly unified accounting --- src/governance/governable.sol | 29 ++--- src/token/OUSD.sol | 203 ++++++++++++++++------------------ test/OUSD.t.sol | 62 ++++++++--- 3 files changed, 151 insertions(+), 143 deletions(-) diff --git a/src/governance/governable.sol b/src/governance/governable.sol index 7a4494c..32c986a 100644 --- a/src/governance/governable.sol +++ b/src/governance/governable.sol @@ -11,30 +11,22 @@ pragma solidity ^0.8.0; contract Governable { // Storage position of the owner and pendingOwner of the contract // keccak256("OUSD.governor"); - bytes32 private constant governorPosition = - 0x7bea13895fa79d2831e0a9e28edede30099005a50d652d8957cf8a607ee6ca4a; + bytes32 private constant governorPosition = 0x7bea13895fa79d2831e0a9e28edede30099005a50d652d8957cf8a607ee6ca4a; // keccak256("OUSD.pending.governor"); bytes32 private constant pendingGovernorPosition = 0x44c4d30b2eaad5130ad70c3ba6972730566f3e6359ab83e800d905c61b1c51db; // keccak256("OUSD.reentry.status"); - bytes32 private constant reentryStatusPosition = - 0x53bf423e48ed90e97d02ab0ebab13b2a235a6bfbe9c321847d5c175333ac4535; + bytes32 private constant reentryStatusPosition = 0x53bf423e48ed90e97d02ab0ebab13b2a235a6bfbe9c321847d5c175333ac4535; // See OpenZeppelin ReentrancyGuard implementation uint256 constant _NOT_ENTERED = 1; uint256 constant _ENTERED = 2; - event PendingGovernorshipTransfer( - address indexed previousGovernor, - address indexed newGovernor - ); + event PendingGovernorshipTransfer(address indexed previousGovernor, address indexed newGovernor); - event GovernorshipTransferred( - address indexed previousGovernor, - address indexed newGovernor - ); + event GovernorshipTransferred(address indexed previousGovernor, address indexed newGovernor); /** * @dev Initializes the contract setting the deployer as the initial Governor. @@ -65,11 +57,7 @@ contract Governable { /** * @dev Returns the address of the pending Governor. */ - function _pendingGovernor() - internal - view - returns (address pendingGovernor) - { + function _pendingGovernor() internal view returns (address pendingGovernor) { bytes32 position = pendingGovernorPosition; // solhint-disable-next-line no-inline-assembly assembly { @@ -157,10 +145,7 @@ contract Governable { * Can only be called by the new Governor. */ function claimGovernance() external { - require( - msg.sender == _pendingGovernor(), - "Only the pending Governor can complete the claim" - ); + require(msg.sender == _pendingGovernor(), "Only the pending Governor can complete the claim"); _changeGovernor(msg.sender); } @@ -173,4 +158,4 @@ contract Governable { emit GovernorshipTransferred(_governor(), _newGovernor); _setGovernor(_newGovernor); } -} \ No newline at end of file +} diff --git a/src/token/OUSD.sol b/src/token/OUSD.sol index a5717e8..a39e04c 100644 --- a/src/token/OUSD.sol +++ b/src/token/OUSD.sol @@ -8,6 +8,7 @@ pragma solidity ^0.8.0; * @author Origin Protocol Inc */ import { Governable } from "../governance/Governable.sol"; +import {console} from "forge-std/Test.sol"; /** * NOTE that this is an ERC20 token but the invariant that the sum of @@ -30,7 +31,9 @@ contract OUSD is Governable { enum RebaseOptions { NotSet, OptOut, - OptIn + OptIn, + YieldDelegationSource, + YieldDelegationTarget } uint256 private constant MAX_SUPPLY = ~uint128(0); // (2^128) - 1 @@ -46,8 +49,8 @@ contract OUSD is Governable { mapping(address => uint256) public nonRebasingCreditsPerToken; mapping(address => RebaseOptions) public rebaseState; mapping(address => uint256) public isUpgraded; - mapping(address => address) public yieldDelegate; - mapping(address => uint256) public yieldDelegateeCount; + mapping(address => address) public yieldTo; + mapping(address => address) public yieldFrom; mapping(address => uint256) public aintMoney; uint256 private constant RESOLUTION_INCREASE = 1e9; @@ -131,7 +134,8 @@ contract OUSD is Governable { returns (uint256) { return - _creditBalances[_account] * 1e18 / _creditsPerToken(_account); + _creditBalances[_account] * 1e18 / _creditsPerToken(_account) + - aintMoney[_account]; } /** @@ -239,32 +243,52 @@ contract OUSD is Governable { address _to, uint256 _value ) internal { - bool isNonRebasingTo = _isNonRebasingAccount(_to); - bool isNonRebasingFrom = _isNonRebasingAccount(_from); + if(_from == _to){ + return; + } + + (int256 fromRebasingCreditsDiff, int256 fromNonRebasingSupplyDiff) + = _adjustAccount(_from, -int256(_value)); + (int256 toRebasingCreditsDiff, int256 toNonRebasingSupplyDiff) + = _adjustAccount(_to, int256(_value)); + + _adjustGlobals( + fromRebasingCreditsDiff + toRebasingCreditsDiff, + fromNonRebasingSupplyDiff + toNonRebasingSupplyDiff + ); + } - // Credits deducted and credited might be different due to the - // differing creditsPerToken used by each account - uint256 creditsCredited = _value * _creditsPerToken(_to) / 1e18; - uint256 creditsDeducted = _value * _creditsPerToken(_from) / 1e18; + function _adjustAccount(address account, int256 balanceChange) internal returns (int256 rebasingCreditsDiff, int256 nonRebasingSupplyDiff) { + int256 currentBalance = int256(balanceOf(account)); + int256 newBalance = currentBalance + balanceChange; + if(newBalance < 0){ + revert("Transfer amount exceeds balance"); // Should never trigger + } + if(_isNonRebasingAccount(account)){ + nonRebasingSupplyDiff = balanceChange; + if(nonRebasingCreditsPerToken[account]!=1e27){ + nonRebasingCreditsPerToken[account] = 1e27; + } + _creditBalances[account] = uint256(newBalance) * 1e9; + } else { + int256 newCredits = ((newBalance) * int256(_rebasingCreditsPerToken) + 1e18 - 1) / 1e18; + rebasingCreditsDiff = newCredits - int256(_creditBalances[account]); + _creditBalances[account] = uint256(newCredits); + } + } - if(_creditBalances[_from] < creditsDeducted) { - revert("Transfer amount exceeds balance"); + function _adjustGlobals(int256 rebasingCreditsDiff, int256 nonRebasingSupplyDiff) internal { + if(rebasingCreditsDiff !=0){ + if (uint256(int256(_rebasingCredits) + rebasingCreditsDiff) < 0){ + revert("underflow"); + } + _rebasingCredits = uint256(int256(_rebasingCredits) + rebasingCreditsDiff); } - _creditBalances[_from] = _creditBalances[_from] - creditsDeducted; - _creditBalances[_to] = _creditBalances[_to] + creditsCredited; - - if (isNonRebasingTo && !isNonRebasingFrom) { - // Transfer to non-rebasing account from rebasing account, credits - // are removed from the non rebasing tally - nonRebasingSupply = nonRebasingSupply + _value; - // Update rebasingCredits by subtracting the deducted amount - _rebasingCredits = _rebasingCredits - creditsDeducted; - } else if (!isNonRebasingTo && isNonRebasingFrom) { - // Transfer to rebasing account from non-rebasing account - // Decreasing non-rebasing credits by the amount that was sent - nonRebasingSupply = nonRebasingSupply - _value; - // Update rebasingCredits by adding the credited amount - _rebasingCredits = _rebasingCredits + creditsCredited; + if(nonRebasingSupplyDiff !=0){ + if (int256(nonRebasingSupply) + nonRebasingSupplyDiff < 0){ + revert("underflow"); + } + nonRebasingSupply = uint256(int256(nonRebasingSupply) + nonRebasingSupplyDiff); } } @@ -364,23 +388,12 @@ contract OUSD is Governable { function _mint(address _account, uint256 _amount) internal nonReentrant { require(_account != address(0), "Mint to the zero address"); - bool isNonRebasingAccount = _isNonRebasingAccount(_account); - - uint256 creditAmount = _amount * _creditsPerToken(_account) / 1e18; - _creditBalances[_account] = _creditBalances[_account] + creditAmount; - - // If the account is non rebasing and doesn't have a set creditsPerToken - // then set it i.e. this is a mint from a fresh contract - if (isNonRebasingAccount) { - nonRebasingSupply = nonRebasingSupply + _amount; - } else { - _rebasingCredits = _rebasingCredits + creditAmount; - } - + (int256 toRebasingCreditsDiff, int256 toNonRebasingSupplyDiff) + = _adjustAccount(_account, int256(_amount)); + _adjustGlobals(toRebasingCreditsDiff, toNonRebasingSupplyDiff); _totalSupply = _totalSupply + _amount; require(_totalSupply < MAX_SUPPLY, "Max supply"); - emit Transfer(address(0), _account, _amount); } @@ -408,29 +421,9 @@ contract OUSD is Governable { return; } - bool isNonRebasingAccount = _isNonRebasingAccount(_account); - uint256 creditAmount = _amount * _creditsPerToken(_account) / 1e18; - uint256 currentCredits = _creditBalances[_account]; - - // Remove the credits, burning rounding errors - if ( - currentCredits == creditAmount || currentCredits - 1 == creditAmount - ) { - // Handle dust from rounding - _creditBalances[_account] = 0; - } else if (currentCredits > creditAmount) { - _creditBalances[_account] = _creditBalances[_account] - creditAmount; - } else { - revert("Remove exceeds balance"); - } - - // Remove from the credit tallies and non-rebasing supply - if (isNonRebasingAccount) { - nonRebasingSupply = nonRebasingSupply - _amount; - } else { - _rebasingCredits = _rebasingCredits - creditAmount; - } - + (int256 toRebasingCreditsDiff, int256 toNonRebasingSupplyDiff) + = _adjustAccount(_account, -int256(_amount)); + _adjustGlobals(toRebasingCreditsDiff, toNonRebasingSupplyDiff); _totalSupply = _totalSupply - _amount; emit Transfer(_account, address(0), _amount); @@ -471,6 +464,7 @@ contract OUSD is Governable { * supply is updated following deployment of frozen yield change. */ function _ensureRebasingMigration(address _account) internal { + // Todo: integrate, deduplicate if (nonRebasingCreditsPerToken[_account] == 0) { emit AccountRebasingDisabled(_account); if (_creditBalances[_account] == 0) { @@ -516,45 +510,37 @@ contract OUSD is Governable { function _rebaseOptIn(address _account) internal { require(_isNonRebasingAccount(_account), "Account has not opted out"); - - // Convert balance into the same amount at the current exchange rate - uint256 newCreditBalance = _creditBalances[_account] - * _rebasingCreditsPerToken - / _creditsPerToken(_account); - - // Decreasing non rebasing supply - nonRebasingSupply = nonRebasingSupply - balanceOf(_account); - - _creditBalances[_account] = newCreditBalance; - - // Increase rebasing credits, totalSupply remains unchanged so no - // adjustment necessary - _rebasingCredits = _rebasingCredits + _creditBalances[_account]; - - rebaseState[_account] = RebaseOptions.OptIn; - - // Delete any fixed credits per token - delete nonRebasingCreditsPerToken[_account]; + uint256 balance = balanceOf(msg.sender); + (int256 beforeRebasingCreditsDiff, int256 beforeNonRebasingSupplyDiff) + = _adjustAccount(msg.sender, -int256(balance)); + + nonRebasingCreditsPerToken[msg.sender] = 0; + rebaseState[msg.sender] = RebaseOptions.OptIn; + + (int256 afterRebasingCreditsDiff, int256 afterNonRebasingSupplyDiff) + = _adjustAccount(msg.sender, int256(balance)); + _adjustGlobals( + beforeRebasingCreditsDiff - afterRebasingCreditsDiff, + beforeNonRebasingSupplyDiff - afterNonRebasingSupplyDiff + ); emit AccountRebasingEnabled(_account); } - /** - * @dev Explicitly mark that an address is non-rebasing. - */ function rebaseOptOut() public nonReentrant { require(!_isNonRebasingAccount(msg.sender), "Account has not opted in"); + uint256 balance = balanceOf(msg.sender); + (int256 beforeRebasingCreditsDiff, int256 beforeNonRebasingSupplyDiff) + = _adjustAccount(msg.sender, -int256(balance)); - // Increase non rebasing supply - nonRebasingSupply = nonRebasingSupply + balanceOf(msg.sender); - // Set fixed credits per token - nonRebasingCreditsPerToken[msg.sender] = _rebasingCreditsPerToken; - - // Decrease rebasing credits, total supply remains unchanged so no - // adjustment necessary - _rebasingCredits = _rebasingCredits - _creditBalances[msg.sender]; - - // Mark explicitly opted out of rebasing + nonRebasingCreditsPerToken[msg.sender] = 1e27; rebaseState[msg.sender] = RebaseOptions.OptOut; + + (int256 afterRebasingCreditsDiff, int256 afterNonRebasingSupplyDiff) + = _adjustAccount(msg.sender, int256(balance)); + _adjustGlobals( + afterRebasingCreditsDiff - beforeRebasingCreditsDiff, + afterNonRebasingSupplyDiff - beforeNonRebasingSupplyDiff + ); emit AccountRebasingDisabled(msg.sender); } @@ -600,21 +586,26 @@ contract OUSD is Governable { function delegateYield(address from, address to) external onlyGovernor() { require(from != to, "Cannot delegate to self"); - require(yieldDelegateeCount[from] == 0, "Cannot delegate from delegatee"); - require(yieldDelegate[to] == address(0), "Cannot delegate to delegator"); - require(yieldDelegate[from] == address(0), "Already delegated"); + require( + yieldFrom[to] == address(0) + && yieldTo[to] == address(0) + && yieldFrom[from] == address(0) + && yieldTo[from] == address(0) + , "Blocked by existing yield delegation"); require(!_isNonRebasingAccount(to), "Must delegate to a rebasing account"); require(_isNonRebasingAccount(from), "Must delegate from a non-rebasing account"); - yieldDelegate[from] = to; - rebaseState[from] = RebaseOptions.OptIn; - rebaseState[to] = RebaseOptions.OptIn; - yieldDelegateeCount[to] += 1; + yieldTo[from] = to; + yieldFrom[to] = from; + rebaseState[from] = RebaseOptions.YieldDelegationSource; + rebaseState[to] = RebaseOptions.YieldDelegationTarget; + // Todo: accounting changes } function undelegateYield(address from) external onlyGovernor() { - require(yieldDelegate[from] != address(0), ""); - yieldDelegateeCount[yieldDelegate[from]] -= 1; - yieldDelegate[from] = address(0); + require(yieldTo[from] != address(0), ""); + yieldFrom[yieldTo[from]] = address(0); + yieldTo[from] = address(0); + // Todo: change rebase state } } \ No newline at end of file diff --git a/test/OUSD.t.sol b/test/OUSD.t.sol index b4f9e80..ee8427e 100644 --- a/test/OUSD.t.sol +++ b/test/OUSD.t.sol @@ -8,17 +8,16 @@ contract CounterTest is Test { OUSD public ousd; address public matt = makeAddr("Matt"); - address public labs = makeAddr("Labs"); + address public labs = makeAddr("NonRebasing"); address public pool = makeAddr("Pool"); address public collector = makeAddr("Collector"); address public attacker = makeAddr("Attacker"); - - + address[] accounts = [matt, attacker, labs, pool, collector]; function setUp() public { ousd = new OUSD(); ousd.initialize("", "", address(this), 314159265358979323846264333); - + ousd.mint(matt, 1000 ether); assertEq(ousd.totalSupply(), 1000 ether); ousd.mint(labs, 1000 ether); @@ -27,6 +26,9 @@ contract CounterTest is Test { ousd.mint(attacker, 1000 ether); assertEq(ousd.totalSupply(), 5000 ether); + vm.prank(labs); + ousd.rebaseOptOut(); + vm.prank(pool); ousd.rebaseOptOut(); @@ -34,21 +36,21 @@ contract CounterTest is Test { ousd.rebaseOptOut(); vm.prank(collector); ousd.rebaseOptIn(); - + + assertEq(ousd.nonRebasingSupply(), 2000 ether); ousd.delegateYield(pool, collector); + assertEq(ousd.nonRebasingSupply(), 1000 ether, "delegate should decrease rebasing"); assertEq(ousd.totalSupply(), 5000 ether); - assertEq(ousd.nonRebasingSupply(), 1000 ether); } function test_ChangeSupply() public { - assertEq(ousd.totalSupply(), 5000 ether); - assertEq(ousd.nonRebasingSupply(), 1000 ether); - ousd.changeSupply(6000 ether); assertEq(ousd.totalSupply(), 6000 ether); assertEq(ousd.nonRebasingSupply(), 1000 ether); + ousd.changeSupply(7000 ether); + assertEq(ousd.totalSupply(), 7000 ether); + assertEq(ousd.nonRebasingSupply(), 1000 ether); } - function test_CanDelegateYield() public { vm.prank(matt); ousd.rebaseOptOut(); @@ -61,18 +63,48 @@ contract CounterTest is Test { } function test_NoDelegateYieldToDelegator() public { - vm.expectRevert("Cannot delegate to delegator"); + vm.expectRevert("Blocked by existing yield delegation"); ousd.delegateYield(matt, pool); } function test_NoDelegateYieldFromReceiver() public { - assertEq(ousd.yieldDelegateeCount(collector),1); - vm.expectRevert("Cannot delegate from delegatee"); + vm.expectRevert("Blocked by existing yield delegation"); ousd.delegateYield(collector, matt); } - + function test_CanUndelegeteYield() public { + assertEq(ousd.yieldTo(pool), collector); ousd.undelegateYield(pool); - assertEq(ousd.yieldDelegate(pool), address(0)); + assertEq(ousd.yieldTo(pool), address(0)); + } + + function testDelegateYield() public { + + ousd.changeSupply(ousd.totalSupply() + 1000 ether); + assertEq(ousd.balanceOf(matt), 100); + + } + + function test_Transfers() external { + for (uint256 i = 0; i < accounts.length; i++) { + for (uint256 j = 0; j < accounts.length; j++) { + console.log("Transferring from ", accounts[i], " to ", accounts[j]); + address from = accounts[i]; + address to = accounts[j]; + uint256 amount = 7 ether + 1231231203815; + uint256 fromBefore = ousd.balanceOf(from); + uint256 toBefore = ousd.balanceOf(to); + uint256 totalSupplyBefore = ousd.totalSupply(); + vm.prank(from); + ousd.transfer(to, amount); + if (from == to) { + assertEq(ousd.balanceOf(from), fromBefore); + } else { + assertEq(ousd.balanceOf(from), fromBefore - amount, "From account balance should decrease"); + assertEq(ousd.balanceOf(to), toBefore + amount, "To account balance should increase"); + } + assertEq(ousd.totalSupply(), totalSupplyBefore); + } + } } }