From 07293d20f2730e5cf3668b72c735b5c8795c9127 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 30 Oct 2024 23:11:05 +0100 Subject: [PATCH 001/110] initial second implementation of rebasing to another account --- contracts/contracts/token/OUSD.sol | 84 ++++++++++++++++++++++++++++-- contracts/test/token/ousd.js | 64 +++++++++++++++++++++++ 2 files changed, 143 insertions(+), 5 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 5f8dba4c46..09730dbf05 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -14,7 +14,8 @@ import { Initializable } from "../utils/Initializable.sol"; import { InitializableERC20Detailed } from "../utils/InitializableERC20Detailed.sol"; import { StableMath } from "../utils/StableMath.sol"; import { Governable } from "../governance/Governable.sol"; - +// TODO DELETE +import "hardhat/console.sol"; /** * NOTE that this is an ERC20 token but the invariant that the sum of * balanceOf(x) for all x is not >= totalSupply(). This is a consequence of the @@ -32,17 +33,35 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { ); event AccountRebasingEnabled(address account); event AccountRebasingDisabled(address account); + event YieldDelegationStart(address fromAccount, address toAccount, uint256 rebasingCreditsPerToken); + event YieldDelegationStop(address fromAccount, address toAccount, uint256 rebasingCreditsPerToken); enum RebaseOptions { NotSet, OptOut, - OptIn + OptIn, + Delegator, + Delegatee } uint256 private constant MAX_SUPPLY = ~uint128(0); // (2^128) - 1 uint256 public _totalSupply; mapping(address => mapping(address => uint256)) private _allowances; address public vaultAddress = address(0); + /** + * This used to represent rebasing credits where balance would be derived using + * global contract `rebasingCreditsPerToken`. Or non rebasing where balance is derived + * using the account specific `nonRebasingCreditsPerToken`. + * + * While the above functionality still stands the _creditBalances alone only partly represents + * the third type of accounts which are part of yield delegation (delegators and delegatees). + * In those cases the _creditBalances express the amount of balance that counts towards the + * yield of a specific account. The `_amountAdjustement` supplements the balances logic and + * subtracts token balances from yield delegatees and adds to yield delegators to make up the + * exact token balances. The `_amountAdjustement` retain their value between rebases. + * TODO (might need more in dept explanation of _amountAdjustement) + * + */ mapping(address => uint256) private _creditBalances; uint256 private _rebasingCredits; uint256 private _rebasingCreditsPerToken; @@ -52,6 +71,10 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { mapping(address => uint256) public nonRebasingCreditsPerToken; mapping(address => RebaseOptions) public rebaseState; mapping(address => uint256) public isUpgraded; + /** + * Facilitates the correct token balances in accounts affected by yield delegation + */ + mapping(address => uint256) private _amountAdjustement; uint256 private constant RESOLUTION_INCREASE = 1e9; @@ -121,9 +144,26 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { override returns (uint256) { - if (_creditBalances[_account] == 0) return 0; - return - _creditBalances[_account].divPrecisely(_creditsPerToken(_account)); + uint256 nonDelegatedBalance; + if (_creditBalances[_account] == 0) nonDelegatedBalance = 0; + else { + nonDelegatedBalance = _creditBalances[_account].divPrecisely(_creditsPerToken(_account)); + } + + return _adjustBalanceForYieldDelegation(_account, nonDelegatedBalance); + } + + function _adjustBalanceForYieldDelegation(address _account, uint256 _nonDelegatedBalance) + internal + view + returns(uint256) + { + if (rebaseState[_account] == RebaseOptions.Delegator) { + return _nonDelegatedBalance + _amountAdjustement[_account]; + } else if (rebaseState[_account] == RebaseOptions.Delegatee) { + return _nonDelegatedBalance - _amountAdjustement[_account]; + } + return _nonDelegatedBalance; } /** @@ -138,6 +178,7 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { view returns (uint256, uint256) { + // TODO add yield delegation uint256 cpt = _creditsPerToken(_account); if (cpt == 1e27) { // For a period before the resolution upgrade, we created all new @@ -167,6 +208,7 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { bool ) { + // TODO add yield delegation return ( _creditBalances[_account], _creditsPerToken(_account), @@ -460,6 +502,7 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { function _isNonRebasingAccount(address _account) internal returns (bool) { bool isContract = Address.isContract(_account); if (isContract && rebaseState[_account] == RebaseOptions.NotSet) { + // TODO: make sure this never executes with yield delegators or delegatees _ensureRebasingMigration(_account); } return nonRebasingCreditsPerToken[_account] > 0; @@ -559,6 +602,37 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { emit AccountRebasingDisabled(msg.sender); } + function governanceDelegateYield(address _accountSource, address _accountReceiver) + public + onlyGovernor + { + if (rebaseState[_accountSource] == RebaseOptions.OptOut) { + _rebaseOptIn(_accountSource); + } + if (rebaseState[_accountReceiver] == RebaseOptions.OptOut) { + _rebaseOptIn(_accountSource); + } + uint256 accountSourceBalance = balanceOf(_accountSource); + + rebaseState[_accountSource] = RebaseOptions.Delegator; + rebaseState[_accountReceiver] = RebaseOptions.Delegatee; + + // delegate all yield and adjust for balances + _creditBalances[_accountReceiver] += _creditBalances[_accountSource]; + _creditBalances[_accountSource] = 0; + _amountAdjustement[_accountSource] = accountSourceBalance; + _amountAdjustement[_accountReceiver] = accountSourceBalance; + + emit YieldDelegationStart(_accountSource, _accountReceiver, _rebasingCreditsPerToken); + } + + function governanceStopYieldDelegation(address _accountSource) + public + onlyGovernor + { + require(false, "Needs implementation"); + } + /** * @dev Modify the supply without minting new tokens. This uses a change in * the exchange rate between "credits" and OUSD tokens to change balances. diff --git a/contracts/test/token/ousd.js b/contracts/test/token/ousd.js index caea0ad059..7b4b70b27f 100644 --- a/contracts/test/token/ousd.js +++ b/contracts/test/token/ousd.js @@ -849,4 +849,68 @@ describe("Token", function () { await checkTransferOut(5); await checkTransferOut(9); }); + + describe.only("Delegating yield", function () { + it("Should delegate rebase to another account", async () => { + let { ousd, vault, matt, josh, anna, usdc, governor } = fixture; + + await ousd.connect(matt).transfer(anna.address, ousdUnits("10")); + await ousd.connect(matt).transfer(josh.address, ousdUnits("10")); + + await expect(josh).has.an.approxBalanceOf("110.00", ousd); + await expect(matt).has.an.approxBalanceOf("80.00", ousd); + await expect(anna).has.an.approxBalanceOf("10", ousd); + + await ousd + .connect(governor) + // matt delegates yield to anna + .governanceDelegateYield(matt.address, anna.address); + + // Transfer USDC into the Vault to simulate yield + await usdc.connect(matt).transfer(vault.address, usdcUnits("200")); + await vault.rebase(); + + await expect(josh).has.an.approxBalanceOf("220.00", ousd); + await expect(matt).has.an.approxBalanceOf("80.00", ousd); + // 10 of own rebase + 80 from matt + 10 existing balance + await expect(anna).has.an.balanceOf("100", ousd); + + await ousd.connect(anna).transfer(josh.address, ousdUnits("10")); + + await expect(josh).has.an.approxBalanceOf("230.00", ousd); + await expect(matt).has.an.approxBalanceOf("80.00", ousd); + await expect(anna).has.an.balanceOf("90", ousd); + }); + + it("Should delegate rebase to another account initially having 0 balance", async () => { + let { ousd, vault, matt, josh, anna, usdc, governor } = fixture; + + await expect(josh).has.an.approxBalanceOf("100.00", ousd); + await expect(matt).has.an.approxBalanceOf("100.00", ousd); + await expect(anna).has.an.balanceOf("0", ousd); + + await ousd + .connect(governor) + // matt delegates yield to anna + .governanceDelegateYield(matt.address, anna.address); + + // Transfer USDC into the Vault to simulate yield + await usdc.connect(matt).transfer(vault.address, usdcUnits("200")); + await vault.rebase(); + + await expect(josh).has.an.approxBalanceOf("200.00", ousd); + await expect(matt).has.an.approxBalanceOf("100.00", ousd); + await expect(anna).has.an.balanceOf("100", ousd); + + await ousd.connect(anna).transfer(josh.address, ousdUnits("10")); + + await expect(josh).has.an.approxBalanceOf("210.00", ousd); + await expect(matt).has.an.approxBalanceOf("100.00", ousd); + await expect(anna).has.an.balanceOf("90", ousd); + }); + + it("should be able to chenge yield delegation", async () => { + //A Should delegate to account B, rebase with profit, delegate to account C and all have correct balances + }); + }); }); From 048b03f6e1e0e015474b38779f87dea9246e84f5 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Thu, 31 Oct 2024 00:46:51 +0100 Subject: [PATCH 002/110] update to core functionality --- contracts/contracts/token/OUSD.sol | 48 ++++++++++++++++++++++++++---- contracts/test/token/ousd.js | 9 ++++++ 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 09730dbf05..da5ba8a83a 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -75,6 +75,14 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { * Facilitates the correct token balances in accounts affected by yield delegation */ mapping(address => uint256) private _amountAdjustement; + /** + * @dev accounts delegating yield to another account + */ + mapping(address => address) public yieldDelegateFrom; + /** + * @dev accounts having yield delegated from another account + */ + mapping(address => address) public yieldDelegateTo; uint256 private constant RESOLUTION_INCREASE = 1e9; @@ -158,6 +166,7 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { view returns(uint256) { + if (rebaseState[_account] == RebaseOptions.Delegator) { return _nonDelegatedBalance + _amountAdjustement[_account]; } else if (rebaseState[_account] == RebaseOptions.Delegatee) { @@ -284,11 +293,38 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { uint256 creditsCredited = _value.mulTruncate(_creditsPerToken(_to)); uint256 creditsDeducted = _value.mulTruncate(_creditsPerToken(_from)); - _creditBalances[_from] = _creditBalances[_from].sub( - creditsDeducted, - "Transfer amount exceeds balance" - ); - _creditBalances[_to] = _creditBalances[_to].add(creditsCredited); + if (rebaseState[_from] == RebaseOptions.Delegator) { + address delegatee = yieldDelegateFrom[_from]; + // adjust delegator and delegatee fixed amounts + _amountAdjustement[_from] -= _value; + _amountAdjustement[delegatee] -= _value; + // also adjust the reduced yield of the delegatee + _creditBalances[delegatee] = _creditBalances[delegatee].sub( + creditsDeducted, + //"Transfer amount exceeds balance" + "Transfer amount exceeds balance delegatee" + ); + } else { + // can not deduct _creditBalances that have been delegated since + // balance checks in transfer & transferFrom prevent it + _creditBalances[_from] = _creditBalances[_from].sub( + creditsDeducted, + "Transfer amount exceeds balance" + ); + + } + + if (rebaseState[_to] == RebaseOptions.Delegator) { + address delegator = yieldDelegateTo[_to]; + // adjust delegator and delegatee fixed amounts + _amountAdjustement[_to] += _value; + _amountAdjustement[delegator] += _value; + // also adjust the additional yield of the delegator + _creditBalances[delegator] = _creditBalances[delegator] + .add(creditsCredited); + } else { + _creditBalances[_to] = _creditBalances[_to].add(creditsCredited); + } if (isNonRebasingTo && !isNonRebasingFrom) { // Transfer to non-rebasing account from rebasing account, credits @@ -622,6 +658,8 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { _creditBalances[_accountSource] = 0; _amountAdjustement[_accountSource] = accountSourceBalance; _amountAdjustement[_accountReceiver] = accountSourceBalance; + yieldDelegateFrom[_accountSource] = _accountReceiver; + yieldDelegateTo[_accountReceiver] = _accountSource; emit YieldDelegationStart(_accountSource, _accountReceiver, _rebasingCreditsPerToken); } diff --git a/contracts/test/token/ousd.js b/contracts/test/token/ousd.js index 7b4b70b27f..45bcce8c7d 100644 --- a/contracts/test/token/ousd.js +++ b/contracts/test/token/ousd.js @@ -880,6 +880,15 @@ describe("Token", function () { await expect(josh).has.an.approxBalanceOf("230.00", ousd); await expect(matt).has.an.approxBalanceOf("80.00", ousd); await expect(anna).has.an.balanceOf("90", ousd); + + console.log("Matt transfering to josh"); + await ousd.connect(matt).transfer(josh.address, ousdUnits("80")); + console.log("Anna transfering to josh"); + await ousd.connect(anna).transfer(josh.address, ousdUnits("90")) + + await expect(josh).has.an.approxBalanceOf("400", ousd); + await expect(matt).has.an.approxBalanceOf("0", ousd); + await expect(anna).has.an.balanceOf("0", ousd); }); it("Should delegate rebase to another account initially having 0 balance", async () => { From 36c599e72e85d7b25b89d15a24bc100da8850b11 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Sun, 3 Nov 2024 18:41:07 +0100 Subject: [PATCH 003/110] add gas fucntion --- contracts/test/token/ousd.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/contracts/test/token/ousd.js b/contracts/test/token/ousd.js index 45bcce8c7d..5cda4d9e8b 100644 --- a/contracts/test/token/ousd.js +++ b/contracts/test/token/ousd.js @@ -851,6 +851,27 @@ describe("Token", function () { }); describe.only("Delegating yield", function () { + // TODO delete below test later + it.only("Figure out gas costs", async () => { + let { ousd, vault, matt, josh, anna, usdc, governor } = fixture; + + await expect(josh).has.an.approxBalanceOf("100", ousd); + await expect(matt).has.an.approxBalanceOf("100", ousd); + await expect(anna).has.an.approxBalanceOf("0", ousd); + + await ousd + .connect(governor) + // matt delegates yield to anna + .governanceDelegateYield(matt.address, anna.address); + + // await ousd.connect(josh).transfer(matt.address, ousdUnits("2")); + // await ousd.connect(josh).transfer(matt.address, ousdUnits("2")); + + await ousd.connect(matt).transfer(josh.address, ousdUnits("2")); + await ousd.connect(matt).transfer(josh.address, ousdUnits("2")); + + }); + it("Should delegate rebase to another account", async () => { let { ousd, vault, matt, josh, anna, usdc, governor } = fixture; From 05d746eceac0395c7139e2ce632784566fbc7b7e Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Sun, 3 Nov 2024 19:30:03 +0100 Subject: [PATCH 004/110] simplify execute --- contracts/contracts/token/OUSD.sol | 69 +++++++++++++----------------- contracts/test/token/ousd.js | 2 +- 2 files changed, 30 insertions(+), 41 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index da5ba8a83a..4d4ad1d3aa 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -76,13 +76,9 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { */ mapping(address => uint256) private _amountAdjustement; /** - * @dev accounts delegating yield to another account + * @dev mapping of accounts delegating yield to another account */ - mapping(address => address) public yieldDelegateFrom; - /** - * @dev accounts having yield delegated from another account - */ - mapping(address => address) public yieldDelegateTo; + mapping(address => address) public yieldDelegation; uint256 private constant RESOLUTION_INCREASE = 1e9; @@ -293,38 +289,8 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { uint256 creditsCredited = _value.mulTruncate(_creditsPerToken(_to)); uint256 creditsDeducted = _value.mulTruncate(_creditsPerToken(_from)); - if (rebaseState[_from] == RebaseOptions.Delegator) { - address delegatee = yieldDelegateFrom[_from]; - // adjust delegator and delegatee fixed amounts - _amountAdjustement[_from] -= _value; - _amountAdjustement[delegatee] -= _value; - // also adjust the reduced yield of the delegatee - _creditBalances[delegatee] = _creditBalances[delegatee].sub( - creditsDeducted, - //"Transfer amount exceeds balance" - "Transfer amount exceeds balance delegatee" - ); - } else { - // can not deduct _creditBalances that have been delegated since - // balance checks in transfer & transferFrom prevent it - _creditBalances[_from] = _creditBalances[_from].sub( - creditsDeducted, - "Transfer amount exceeds balance" - ); - - } - - if (rebaseState[_to] == RebaseOptions.Delegator) { - address delegator = yieldDelegateTo[_to]; - // adjust delegator and delegatee fixed amounts - _amountAdjustement[_to] += _value; - _amountAdjustement[delegator] += _value; - // also adjust the additional yield of the delegator - _creditBalances[delegator] = _creditBalances[delegator] - .add(creditsCredited); - } else { - _creditBalances[_to] = _creditBalances[_to].add(creditsCredited); - } + _adjustCreditsForAccount(_from, -int256(creditsDeducted), -int256(_value)); + _adjustCreditsForAccount(_to, int256(creditsCredited), int256(_value)); if (isNonRebasingTo && !isNonRebasingFrom) { // Transfer to non-rebasing account from rebasing account, credits @@ -341,6 +307,30 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { } } + function _adjustCreditsForAccount(address _account, int256 _creditsDiff, int256 _valueDiff) + internal + { + address alterCreditsAccount = _account; + // only when transferring to/from the Delegators the amount adjustments need to happen + if (rebaseState[_account] == RebaseOptions.Delegator) { + address delegatee = yieldDelegation[_account]; + // adjust delegator and delegatee fixed amounts + _amountAdjustement[_account] = uint256(int256(_amountAdjustement[_account]) + _valueDiff); + _amountAdjustement[delegatee] = uint256(int256(_amountAdjustement[delegatee]) + _valueDiff); + + // also adjust the reduced yield of the delegatee. Even though creditsDeducted + // is derived using Delegator's _creditsPerToken it is ok since both the delegator + // and delegatee operate with contract's global creditsPerToken + alterCreditsAccount = delegatee; + } + + uint256 creditsBalance = _creditBalances[alterCreditsAccount]; + if ((int256(creditsBalance) + _creditsDiff) < 0) { + revert("Transfer exceeds balance"); + } + _creditBalances[alterCreditsAccount] = uint256(int256(creditsBalance) + _creditsDiff); + } + /** * @dev Function to check the amount of tokens that _owner has allowed to * `_spender`. @@ -658,8 +648,7 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { _creditBalances[_accountSource] = 0; _amountAdjustement[_accountSource] = accountSourceBalance; _amountAdjustement[_accountReceiver] = accountSourceBalance; - yieldDelegateFrom[_accountSource] = _accountReceiver; - yieldDelegateTo[_accountReceiver] = _accountSource; + yieldDelegation[_accountSource] = _accountReceiver; emit YieldDelegationStart(_accountSource, _accountReceiver, _rebasingCreditsPerToken); } diff --git a/contracts/test/token/ousd.js b/contracts/test/token/ousd.js index 5cda4d9e8b..c76a41d615 100644 --- a/contracts/test/token/ousd.js +++ b/contracts/test/token/ousd.js @@ -852,7 +852,7 @@ describe("Token", function () { describe.only("Delegating yield", function () { // TODO delete below test later - it.only("Figure out gas costs", async () => { + it("Figure out gas costs", async () => { let { ousd, vault, matt, josh, anna, usdc, governor } = fixture; await expect(josh).has.an.approxBalanceOf("100", ousd); From 28d83f96e93b706ef822537bb03e8b6db3e1fe7c Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Sun, 3 Nov 2024 19:39:07 +0100 Subject: [PATCH 005/110] simplify mint and burn --- contracts/contracts/token/OUSD.sol | 17 +++++++---------- contracts/test/token/ousd.js | 4 ++-- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 4d4ad1d3aa..90e9f298c6 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -432,7 +432,7 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { bool isNonRebasingAccount = _isNonRebasingAccount(_account); uint256 creditAmount = _amount.mulTruncate(_creditsPerToken(_account)); - _creditBalances[_account] = _creditBalances[_account].add(creditAmount); + _adjustCreditsForAccount(_account, int256(creditAmount), int256(_amount)); // 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 @@ -478,15 +478,12 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { 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].sub( - creditAmount - ); + if (currentCredits == creditAmount + 1) { + creditAmount += 1; + } + + if (currentCredits >= creditAmount) { + _adjustCreditsForAccount(_account, -int256(creditAmount), -int256(_amount)); } else { revert("Remove exceeds balance"); } diff --git a/contracts/test/token/ousd.js b/contracts/test/token/ousd.js index c76a41d615..f3ecc01109 100644 --- a/contracts/test/token/ousd.js +++ b/contracts/test/token/ousd.js @@ -4,7 +4,7 @@ const { utils } = require("ethers"); const { daiUnits, ousdUnits, usdcUnits, isFork } = require("../helpers"); -describe("Token", function () { +describe.only("Token", function () { if (isFork) { this.timeout(0); } @@ -850,7 +850,7 @@ describe("Token", function () { await checkTransferOut(9); }); - describe.only("Delegating yield", function () { + describe("Delegating yield", function () { // TODO delete below test later it("Figure out gas costs", async () => { let { ousd, vault, matt, josh, anna, usdc, governor } = fixture; From 45f476bad3dfdfa3c80ab2e7245c9edc2527140f Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Thu, 7 Nov 2024 05:47:02 +0100 Subject: [PATCH 006/110] initial version of Daniel's yield delegation implementation --- contracts/contracts/flipper/Flipper.sol | 20 +- contracts/contracts/token/OETH.sol | 10 + contracts/contracts/token/OETHBase.sol | 19 +- contracts/contracts/token/OUSD.sol | 534 +++++++++++------------- 4 files changed, 274 insertions(+), 309 deletions(-) diff --git a/contracts/contracts/flipper/Flipper.sol b/contracts/contracts/flipper/Flipper.sol index 06fc0568d6..04592d0bfb 100644 --- a/contracts/contracts/flipper/Flipper.sol +++ b/contracts/contracts/flipper/Flipper.sol @@ -19,7 +19,7 @@ contract Flipper is Governable { // Settable coin addresses allow easy testing and use of mock currencies. IERC20 immutable dai; - OUSD immutable ousd; + address immutable ousd; IERC20 immutable usdc; Tether immutable usdt; @@ -37,7 +37,7 @@ contract Flipper is Governable { require(address(_usdc) != address(0)); require(address(_usdt) != address(0)); dai = IERC20(_dai); - ousd = OUSD(_ousd); + ousd = _ousd; usdc = IERC20(_usdc); usdt = Tether(_usdt); } @@ -54,7 +54,7 @@ contract Flipper is Governable { dai.transferFrom(msg.sender, address(this), amount), "DAI transfer failed" ); - require(ousd.transfer(msg.sender, amount), "OUSD transfer failed"); + require(IERC20(ousd).transfer(msg.sender, amount), "OUSD transfer failed"); } /// @notice Sell OUSD for Dai @@ -63,7 +63,7 @@ contract Flipper is Governable { require(amount <= MAXIMUM_PER_TRADE, "Amount too large"); require(dai.transfer(msg.sender, amount), "DAI transfer failed"); require( - ousd.transferFrom(msg.sender, address(this), amount), + IERC20(ousd).transferFrom(msg.sender, address(this), amount), "OUSD transfer failed" ); } @@ -77,7 +77,7 @@ contract Flipper is Governable { usdc.transferFrom(msg.sender, address(this), amount / 1e12), "USDC transfer failed" ); - require(ousd.transfer(msg.sender, amount), "OUSD transfer failed"); + require(IERC20(ousd).transfer(msg.sender, amount), "OUSD transfer failed"); } /// @notice Sell OUSD for USDC @@ -89,7 +89,7 @@ contract Flipper is Governable { "USDC transfer failed" ); require( - ousd.transferFrom(msg.sender, address(this), amount), + IERC20(ousd).transferFrom(msg.sender, address(this), amount), "OUSD transfer failed" ); } @@ -102,7 +102,7 @@ contract Flipper is Governable { // USDT does not return a boolean and reverts, // so no need for a require. usdt.transferFrom(msg.sender, address(this), amount / 1e12); - require(ousd.transfer(msg.sender, amount), "OUSD transfer failed"); + require(IERC20(ousd).transfer(msg.sender, amount), "OUSD transfer failed"); } /// @notice Sell OUSD for USDT @@ -113,7 +113,7 @@ contract Flipper is Governable { // so no need for a require. usdt.transfer(msg.sender, amount / 1e12); require( - ousd.transferFrom(msg.sender, address(this), amount), + IERC20(ousd).transferFrom(msg.sender, address(this), amount), "OUSD transfer failed" ); } @@ -125,7 +125,7 @@ contract Flipper is Governable { /// @dev Opting into yield reduces the gas cost per transfer by about 4K, since /// ousd needs to do less accounting and one less storage write. function rebaseOptIn() external onlyGovernor nonReentrant { - ousd.rebaseOptIn(); + OUSD(ousd).rebaseOptIn(); } /// @notice Owner function to withdraw a specific amount of a token @@ -142,7 +142,7 @@ contract Flipper is Governable { /// again by transferring assets to the contract. function withdrawAll() external onlyGovernor nonReentrant { IERC20(dai).safeTransfer(_governor(), dai.balanceOf(address(this))); - IERC20(ousd).safeTransfer(_governor(), ousd.balanceOf(address(this))); + IERC20(ousd).safeTransfer(_governor(), IERC20(ousd).balanceOf(address(this))); IERC20(address(usdt)).safeTransfer( _governor(), usdt.balanceOf(address(this)) diff --git a/contracts/contracts/token/OETH.sol b/contracts/contracts/token/OETH.sol index 6c6b907b5a..85f9572769 100644 --- a/contracts/contracts/token/OETH.sol +++ b/contracts/contracts/token/OETH.sol @@ -8,5 +8,15 @@ import { OUSD } from "./OUSD.sol"; * @author Origin Protocol Inc */ contract OETH is OUSD { + function symbol() external override pure returns (string memory) { + return "OETH"; + } + function name() external override pure returns (string memory) { + return "Origin Ether"; + } + + function decimals() external override pure returns (uint8) { + return 18; + } } diff --git a/contracts/contracts/token/OETHBase.sol b/contracts/contracts/token/OETHBase.sol index 1cf3f653ca..91419f0d92 100644 --- a/contracts/contracts/token/OETHBase.sol +++ b/contracts/contracts/token/OETHBase.sol @@ -2,20 +2,21 @@ pragma solidity ^0.8.0; import { OUSD } from "./OUSD.sol"; -import { InitializableERC20Detailed } from "../utils/InitializableERC20Detailed.sol"; /** * @title OETH Token Contract * @author Origin Protocol Inc */ contract OETHBase is OUSD { - /** - * @dev OETHb is already intialized on Base. So `initialize` - * cannot be used again. And the `name` and `symbol` - * methods aren't `virtual`. That's the reason this - * function exists. - */ - function initialize2() external onlyGovernor { - InitializableERC20Detailed._initialize("Super OETH", "superOETHb", 18); + function symbol() external override pure returns (string memory) { + return "superOETHb"; + } + + function name() external override pure returns (string memory) { + return "Super OETH"; + } + + function decimals() external override pure returns (uint8) { + return 18; } } diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 90e9f298c6..a600b3d53b 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -7,24 +7,16 @@ pragma solidity ^0.8.0; * @dev Implements an elastic supply * @author Origin Protocol Inc */ -import { SafeMath } from "@openzeppelin/contracts/utils/math/SafeMath.sol"; -import { Address } from "@openzeppelin/contracts/utils/Address.sol"; - -import { Initializable } from "../utils/Initializable.sol"; -import { InitializableERC20Detailed } from "../utils/InitializableERC20Detailed.sol"; -import { StableMath } from "../utils/StableMath.sol"; import { Governable } from "../governance/Governable.sol"; -// TODO DELETE -import "hardhat/console.sol"; +//import {console} from "forge-std/Test.sol"; + /** * NOTE that this is an ERC20 token but the invariant that the sum of * balanceOf(x) for all x is not >= totalSupply(). This is a consequence of the * rebasing design. Any integrations with OUSD should be aware. */ -contract OUSD is Initializable, InitializableERC20Detailed, Governable { - using SafeMath for uint256; - using StableMath for uint256; +contract OUSD is Governable { event TotalSupplyUpdatedHighres( uint256 totalSupply, @@ -33,66 +25,57 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { ); event AccountRebasingEnabled(address account); event AccountRebasingDisabled(address account); - event YieldDelegationStart(address fromAccount, address toAccount, uint256 rebasingCreditsPerToken); - event YieldDelegationStop(address fromAccount, address toAccount, uint256 rebasingCreditsPerToken); + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); enum RebaseOptions { NotSet, - OptOut, - OptIn, - Delegator, - Delegatee + StdNonRebasing, + StdRebasing, + YieldDelegationSource, + YieldDelegationTarget } uint256 private constant MAX_SUPPLY = ~uint128(0); // (2^128) - 1 uint256 public _totalSupply; mapping(address => mapping(address => uint256)) private _allowances; address public vaultAddress = address(0); - /** - * This used to represent rebasing credits where balance would be derived using - * global contract `rebasingCreditsPerToken`. Or non rebasing where balance is derived - * using the account specific `nonRebasingCreditsPerToken`. - * - * While the above functionality still stands the _creditBalances alone only partly represents - * the third type of accounts which are part of yield delegation (delegators and delegatees). - * In those cases the _creditBalances express the amount of balance that counts towards the - * yield of a specific account. The `_amountAdjustement` supplements the balances logic and - * subtracts token balances from yield delegatees and adds to yield delegators to make up the - * exact token balances. The `_amountAdjustement` retain their value between rebases. - * TODO (might need more in dept explanation of _amountAdjustement) - * - */ mapping(address => uint256) private _creditBalances; - uint256 private _rebasingCredits; + uint256 private _rebasingCredits; // Sum of all rebasing credits (_creditBalances for rebasing accounts) uint256 private _rebasingCreditsPerToken; - // Frozen address/credits are non rebasing (value is held in contracts which - // do not receive yield unless they explicitly opt in) - uint256 public nonRebasingSupply; - mapping(address => uint256) public nonRebasingCreditsPerToken; + uint256 public nonRebasingSupply; // All nonrebasing balances + mapping(address => uint256) private alternativeCreditsPerToken; mapping(address => RebaseOptions) public rebaseState; mapping(address => uint256) public isUpgraded; - /** - * Facilitates the correct token balances in accounts affected by yield delegation - */ - mapping(address => uint256) private _amountAdjustement; - /** - * @dev mapping of accounts delegating yield to another account - */ - mapping(address => address) public yieldDelegation; + mapping(address => address) public yieldTo; + mapping(address => address) public yieldFrom; uint256 private constant RESOLUTION_INCREASE = 1e9; function initialize( - string calldata _nameArg, - string calldata _symbolArg, + string calldata, + string calldata, address _vaultAddress, uint256 _initialCreditsPerToken - ) external onlyGovernor initializer { - InitializableERC20Detailed._initialize(_nameArg, _symbolArg, 18); + ) external onlyGovernor { + require(vaultAddress == address(0), "Already initialized"); + require(_rebasingCreditsPerToken == 0, "Already initialized"); _rebasingCreditsPerToken = _initialCreditsPerToken; vaultAddress = _vaultAddress; } + function symbol() external virtual pure returns (string memory) { + return "OUSD"; + } + + function name() external virtual pure returns (string memory) { + return "Origin Dollar"; + } + + function decimals() external virtual pure returns (uint8) { + return 18; + } + /** * @dev Verifies that the caller is the Vault contract */ @@ -104,36 +87,36 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { /** * @return The total supply of OUSD. */ - function totalSupply() public view override returns (uint256) { + function totalSupply() public view returns (uint256) { return _totalSupply; } /** - * @return Low resolution rebasingCreditsPerToken + * @return High resolution rebasingCreditsPerToken */ - function rebasingCreditsPerToken() public view returns (uint256) { - return _rebasingCreditsPerToken / RESOLUTION_INCREASE; + function rebasingCreditsPerTokenHighres() public view returns (uint256) { + return _rebasingCreditsPerToken; } /** - * @return Low resolution total number of rebasing credits + * @return Low resolution rebasingCreditsPerToken */ - function rebasingCredits() public view returns (uint256) { - return _rebasingCredits / RESOLUTION_INCREASE; + function rebasingCreditsPerToken() public view returns (uint256) { + return _rebasingCreditsPerToken / RESOLUTION_INCREASE; } /** - * @return High resolution rebasingCreditsPerToken + * @return High resolution total number of rebasing credits */ - function rebasingCreditsPerTokenHighres() public view returns (uint256) { - return _rebasingCreditsPerToken; + function rebasingCreditsHighres() public view returns (uint256) { + return _rebasingCredits; } /** - * @return High resolution total number of rebasing credits + * @return Low resolution total number of rebasing credits */ - function rebasingCreditsHighres() public view returns (uint256) { - return _rebasingCredits; + function rebasingCredits() public view returns (uint256) { + return _rebasingCredits / RESOLUTION_INCREASE; } /** @@ -145,30 +128,19 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { function balanceOf(address _account) public view - override returns (uint256) { - uint256 nonDelegatedBalance; - if (_creditBalances[_account] == 0) nonDelegatedBalance = 0; - else { - nonDelegatedBalance = _creditBalances[_account].divPrecisely(_creditsPerToken(_account)); - } - - return _adjustBalanceForYieldDelegation(_account, nonDelegatedBalance); - } - - function _adjustBalanceForYieldDelegation(address _account, uint256 _nonDelegatedBalance) - internal - view - returns(uint256) - { - - if (rebaseState[_account] == RebaseOptions.Delegator) { - return _nonDelegatedBalance + _amountAdjustement[_account]; - } else if (rebaseState[_account] == RebaseOptions.Delegatee) { - return _nonDelegatedBalance - _amountAdjustement[_account]; + RebaseOptions state = rebaseState[_account]; + if(state == RebaseOptions.YieldDelegationSource){ + // Saves a slot read when transfering to or from a yield delegating source + // since we know creditBalances equals the balance. + return _creditBalances[_account]; } - return _nonDelegatedBalance; + uint256 baseBalance = _creditBalances[_account] * 1e18 / _creditsPerToken(_account); + if (state == RebaseOptions.YieldDelegationTarget) { + return baseBalance - _creditBalances[yieldFrom[_account]]; + } + return baseBalance; } /** @@ -183,7 +155,6 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { view returns (uint256, uint256) { - // TODO add yield delegation uint256 cpt = _creditsPerToken(_account); if (cpt == 1e27) { // For a period before the resolution upgrade, we created all new @@ -213,7 +184,6 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { bool ) { - // TODO add yield delegation return ( _creditBalances[_account], _creditsPerToken(_account), @@ -221,6 +191,11 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { ); } + // Backwards compatible view + function nonRebasingCreditsPerToken(address _account) external view returns (uint256) { + return alternativeCreditsPerToken[_account]; + } + /** * @dev Transfer tokens to a specified address. * @param _to the address to transfer to. @@ -229,14 +204,9 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { */ function transfer(address _to, uint256 _value) public - override returns (bool) { require(_to != address(0), "Transfer to zero address"); - require( - _value <= balanceOf(msg.sender), - "Transfer greater than balance" - ); _executeTransfer(msg.sender, _to, _value); @@ -255,13 +225,10 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { address _from, address _to, uint256 _value - ) public override returns (bool) { + ) public returns (bool) { require(_to != address(0), "Transfer to zero address"); - require(_value <= balanceOf(_from), "Transfer greater than balance"); - _allowances[_from][msg.sender] = _allowances[_from][msg.sender].sub( - _value - ); + _allowances[_from][msg.sender] = _allowances[_from][msg.sender] - _value; _executeTransfer(_from, _to, _value); @@ -281,54 +248,68 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { address _to, uint256 _value ) internal { - bool isNonRebasingTo = _isNonRebasingAccount(_to); - bool isNonRebasingFrom = _isNonRebasingAccount(_from); - - // Credits deducted and credited might be different due to the - // differing creditsPerToken used by each account - uint256 creditsCredited = _value.mulTruncate(_creditsPerToken(_to)); - uint256 creditsDeducted = _value.mulTruncate(_creditsPerToken(_from)); - - _adjustCreditsForAccount(_from, -int256(creditsDeducted), -int256(_value)); - _adjustCreditsForAccount(_to, int256(creditsCredited), int256(_value)); - - if (isNonRebasingTo && !isNonRebasingFrom) { - // Transfer to non-rebasing account from rebasing account, credits - // are removed from the non rebasing tally - nonRebasingSupply = nonRebasingSupply.add(_value); - // Update rebasingCredits by subtracting the deducted amount - _rebasingCredits = _rebasingCredits.sub(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.sub(_value); - // Update rebasingCredits by adding the credited amount - _rebasingCredits = _rebasingCredits.add(creditsCredited); + 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 + ); } - function _adjustCreditsForAccount(address _account, int256 _creditsDiff, int256 _valueDiff) - internal - { - address alterCreditsAccount = _account; - // only when transferring to/from the Delegators the amount adjustments need to happen - if (rebaseState[_account] == RebaseOptions.Delegator) { - address delegatee = yieldDelegation[_account]; - // adjust delegator and delegatee fixed amounts - _amountAdjustement[_account] = uint256(int256(_amountAdjustement[_account]) + _valueDiff); - _amountAdjustement[delegatee] = uint256(int256(_amountAdjustement[delegatee]) + _valueDiff); - - // also adjust the reduced yield of the delegatee. Even though creditsDeducted - // is derived using Delegator's _creditsPerToken it is ok since both the delegator - // and delegatee operate with contract's global creditsPerToken - alterCreditsAccount = delegatee; + function _adjustAccount(address account, int256 balanceChange) internal returns (int256 rebasingCreditsDiff, int256 nonRebasingSupplyDiff) { + RebaseOptions state = rebaseState[account]; + int256 currentBalance = int256(balanceOf(account)); + uint256 newBalance = uint256(int256(currentBalance) + int256(balanceChange)); + if(newBalance < 0){ + revert("Transfer amount exceeds balance"); } + if (state == RebaseOptions.YieldDelegationSource) { + address target = yieldTo[account]; + uint256 targetOldBalance = balanceOf(target); + uint256 targetNewCredits = _balanceToRebasingCredits(targetOldBalance + newBalance); + rebasingCreditsDiff = int256(targetNewCredits) - int256(_creditBalances[target]); + + _creditBalances[account] = newBalance; + _creditBalances[target] = targetNewCredits; + alternativeCreditsPerToken[account] = 1e18; + + } else if (state == RebaseOptions.YieldDelegationTarget) { + uint256 newCredits = _balanceToRebasingCredits(newBalance + _creditBalances[yieldFrom[account]]); + rebasingCreditsDiff = int256(newCredits) - int256(_creditBalances[account]); + _creditBalances[account] = newCredits; + + } else if(_isNonRebasingAccount(account)){ + nonRebasingSupplyDiff = balanceChange; + alternativeCreditsPerToken[account] = 1e18; + _creditBalances[account] = newBalance; - uint256 creditsBalance = _creditBalances[alterCreditsAccount]; - if ((int256(creditsBalance) + _creditsDiff) < 0) { - revert("Transfer exceeds balance"); + } else { + uint256 newCredits = _balanceToRebasingCredits(newBalance); + rebasingCreditsDiff = int256(newCredits) - int256(_creditBalances[account]); + _creditBalances[account] = newCredits; + } + } + + function _adjustGlobals(int256 rebasingCreditsDiff, int256 nonRebasingSupplyDiff) internal { + if(rebasingCreditsDiff !=0){ + if (uint256(int256(_rebasingCredits) + rebasingCreditsDiff) < 0){ + revert("rebasingCredits underflow"); + } + _rebasingCredits = uint256(int256(_rebasingCredits) + rebasingCreditsDiff); + } + if(nonRebasingSupplyDiff !=0){ + if (int256(nonRebasingSupply) + nonRebasingSupplyDiff < 0){ + revert("nonRebasingSupply underflow"); + } + nonRebasingSupply = uint256(int256(nonRebasingSupply) + nonRebasingSupplyDiff); } - _creditBalances[alterCreditsAccount] = uint256(int256(creditsBalance) + _creditsDiff); } /** @@ -341,7 +322,6 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { function allowance(address _owner, address _spender) public view - override returns (uint256) { return _allowances[_owner][_spender]; @@ -349,20 +329,12 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { /** * @dev Approve the passed address to spend the specified amount of tokens - * on behalf of msg.sender. This method is included for ERC20 - * compatibility. `increaseAllowance` and `decreaseAllowance` should be - * used instead. - * - * Changing an allowance with this method brings the risk that someone - * may transfer both the old and the new allowance - if they are both - * greater than zero - if a transfer transaction is mined before the - * later approve() call is mined. + * on behalf of msg.sender. * @param _spender The address which will spend the funds. * @param _value The amount of tokens to be spent. */ function approve(address _spender, uint256 _value) public - override returns (bool) { _allowances[msg.sender][_spender] = _value; @@ -382,9 +354,9 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { public returns (bool) { - _allowances[msg.sender][_spender] = _allowances[msg.sender][_spender] - .add(_addedValue); - emit Approval(msg.sender, _spender, _allowances[msg.sender][_spender]); + uint256 updatedAllowance = _allowances[msg.sender][_spender] + _addedValue; + _allowances[msg.sender][_spender] = updatedAllowance; + emit Approval(msg.sender, _spender, updatedAllowance); return true; } @@ -400,12 +372,14 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { returns (bool) { uint256 oldValue = _allowances[msg.sender][_spender]; + uint256 newValue; if (_subtractedValue >= oldValue) { - _allowances[msg.sender][_spender] = 0; + newValue = 0; } else { - _allowances[msg.sender][_spender] = oldValue.sub(_subtractedValue); + newValue = oldValue - _subtractedValue; } - emit Approval(msg.sender, _spender, _allowances[msg.sender][_spender]); + _allowances[msg.sender][_spender] = newValue; + emit Approval(msg.sender, _spender, newValue); return true; } @@ -429,23 +403,14 @@ contract OUSD is Initializable, InitializableERC20Detailed, 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.mulTruncate(_creditsPerToken(_account)); - _adjustCreditsForAccount(_account, int256(creditAmount), int256(_amount)); - - // 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.add(_amount); - } else { - _rebasingCredits = _rebasingCredits.add(creditAmount); - } - - _totalSupply = _totalSupply.add(_amount); + // Account + (int256 toRebasingCreditsDiff, int256 toNonRebasingSupplyDiff) + = _adjustAccount(_account, int256(_amount)); + // Globals + _adjustGlobals(toRebasingCreditsDiff, toNonRebasingSupplyDiff); + _totalSupply = _totalSupply + _amount; require(_totalSupply < MAX_SUPPLY, "Max supply"); - emit Transfer(address(0), _account, _amount); } @@ -473,29 +438,12 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { return; } - bool isNonRebasingAccount = _isNonRebasingAccount(_account); - uint256 creditAmount = _amount.mulTruncate(_creditsPerToken(_account)); - uint256 currentCredits = _creditBalances[_account]; - - // Remove the credits, burning rounding errors - if (currentCredits == creditAmount + 1) { - creditAmount += 1; - } - - if (currentCredits >= creditAmount) { - _adjustCreditsForAccount(_account, -int256(creditAmount), -int256(_amount)); - } else { - revert("Remove exceeds balance"); - } - - // Remove from the credit tallies and non-rebasing supply - if (isNonRebasingAccount) { - nonRebasingSupply = nonRebasingSupply.sub(_amount); - } else { - _rebasingCredits = _rebasingCredits.sub(creditAmount); - } - - _totalSupply = _totalSupply.sub(_amount); + // Account + (int256 toRebasingCreditsDiff, int256 toNonRebasingSupplyDiff) + = _adjustAccount(_account, -int256(_amount)); + // Globals + _adjustGlobals(toRebasingCreditsDiff, toNonRebasingSupplyDiff); + _totalSupply = _totalSupply - _amount; emit Transfer(_account, address(0), _amount); } @@ -510,8 +458,8 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { view returns (uint256) { - if (nonRebasingCreditsPerToken[_account] != 0) { - return nonRebasingCreditsPerToken[_account]; + if (alternativeCreditsPerToken[_account] != 0) { + return alternativeCreditsPerToken[_account]; } else { return _rebasingCreditsPerToken; } @@ -523,38 +471,19 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { * @param _account Address of the account. */ function _isNonRebasingAccount(address _account) internal returns (bool) { - bool isContract = Address.isContract(_account); - if (isContract && rebaseState[_account] == RebaseOptions.NotSet) { - // TODO: make sure this never executes with yield delegators or delegatees - _ensureRebasingMigration(_account); + bool isContract = _account.code.length > 0; + if (isContract && rebaseState[_account] == RebaseOptions.NotSet && alternativeCreditsPerToken[_account] != 0) { + _rebaseOptOut(msg.sender); } - return nonRebasingCreditsPerToken[_account] > 0; + return alternativeCreditsPerToken[_account] > 0; } - /** - * @dev Ensures internal account for rebasing and non-rebasing credits and - * supply is updated following deployment of frozen yield change. - */ - function _ensureRebasingMigration(address _account) internal { - if (nonRebasingCreditsPerToken[_account] == 0) { - emit AccountRebasingDisabled(_account); - if (_creditBalances[_account] == 0) { - // Since there is no existing balance, we can directly set to - // high resolution, and do not have to do any other bookkeeping - nonRebasingCreditsPerToken[_account] = 1e27; - } else { - // Migrate an existing account: - - // Set fixed credits per token for this account - nonRebasingCreditsPerToken[_account] = _rebasingCreditsPerToken; - // Update non rebasing supply - nonRebasingSupply = nonRebasingSupply.add(balanceOf(_account)); - // Update credit tallies - _rebasingCredits = _rebasingCredits.sub( - _creditBalances[_account] - ); - } - } + function _balanceToRebasingCredits(uint256 balance) internal view returns (uint256) { + // Rounds up, because we need to ensure that accounts allways have + // at least the balance that they should have. + // Note this should always be used on an absolute account value, + // not on a possibly negative diff, because then the rounding would be wrong. + return ((balance) * _rebasingCreditsPerToken + 1e18 - 1) / 1e18; } /** @@ -582,79 +511,47 @@ contract OUSD is Initializable, InitializableERC20Detailed, 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] - .mul(_rebasingCreditsPerToken) - .div(_creditsPerToken(_account)); - - // Decreasing non rebasing supply - nonRebasingSupply = nonRebasingSupply.sub(balanceOf(_account)); - - _creditBalances[_account] = newCreditBalance; + // TODO below line fails when deploying core001 + //require(alternativeCreditsPerToken[_account] != 0, "Account must be non-rebasing"); + RebaseOptions state = rebaseState[_account]; + require(state == RebaseOptions.StdNonRebasing || state == RebaseOptions.NotSet, "Only standard non-rebasing accounts can opt out"); - // Increase rebasing credits, totalSupply remains unchanged so no - // adjustment necessary - _rebasingCredits = _rebasingCredits.add(_creditBalances[_account]); + uint256 balance = balanceOf(msg.sender); + + // Account + rebaseState[msg.sender] = RebaseOptions.StdRebasing; + alternativeCreditsPerToken[msg.sender] = 0; + _creditBalances[msg.sender] = _balanceToRebasingCredits(balance); - rebaseState[_account] = RebaseOptions.OptIn; + // Globals + nonRebasingSupply -= balance; + _rebasingCredits += _creditBalances[msg.sender]; - // Delete any fixed credits per token - delete nonRebasingCreditsPerToken[_account]; 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"); - - // Increase non rebasing supply - nonRebasingSupply = nonRebasingSupply.add(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.sub(_creditBalances[msg.sender]); - - // Mark explicitly opted out of rebasing - rebaseState[msg.sender] = RebaseOptions.OptOut; - emit AccountRebasingDisabled(msg.sender); + _rebaseOptOut(msg.sender); } - function governanceDelegateYield(address _accountSource, address _accountReceiver) - public - onlyGovernor - { - if (rebaseState[_accountSource] == RebaseOptions.OptOut) { - _rebaseOptIn(_accountSource); - } - if (rebaseState[_accountReceiver] == RebaseOptions.OptOut) { - _rebaseOptIn(_accountSource); - } - uint256 accountSourceBalance = balanceOf(_accountSource); - - rebaseState[_accountSource] = RebaseOptions.Delegator; - rebaseState[_accountReceiver] = RebaseOptions.Delegatee; - - // delegate all yield and adjust for balances - _creditBalances[_accountReceiver] += _creditBalances[_accountSource]; - _creditBalances[_accountSource] = 0; - _amountAdjustement[_accountSource] = accountSourceBalance; - _amountAdjustement[_accountReceiver] = accountSourceBalance; - yieldDelegation[_accountSource] = _accountReceiver; + function _rebaseOptOut(address _account) internal { + require(alternativeCreditsPerToken[_account] == 0, "Account must be rebasing"); + RebaseOptions state = rebaseState[_account]; + require(state == RebaseOptions.StdRebasing || state == RebaseOptions.NotSet, "Only standard rebasing accounts can opt out"); + + uint256 oldCredits = _creditBalances[_account]; + uint256 balance = balanceOf(_account); + + // Account + rebaseState[_account] = RebaseOptions.StdNonRebasing; + alternativeCreditsPerToken[_account] = 1e18; + _creditBalances[_account] = balance; - emit YieldDelegationStart(_accountSource, _accountReceiver, _rebasingCreditsPerToken); - } + // Globals + nonRebasingSupply += balance; + _rebasingCredits -= oldCredits; - function governanceStopYieldDelegation(address _accountSource) - public - onlyGovernor - { - require(false, "Needs implementation"); + emit AccountRebasingDisabled(_account); } /** @@ -682,15 +579,13 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { ? MAX_SUPPLY : _newTotalSupply; - _rebasingCreditsPerToken = _rebasingCredits.divPrecisely( - _totalSupply.sub(nonRebasingSupply) - ); + _rebasingCreditsPerToken = _rebasingCredits + * 1e18 / (_totalSupply - nonRebasingSupply); require(_rebasingCreditsPerToken > 0, "Invalid change in supply"); - _totalSupply = _rebasingCredits - .divPrecisely(_rebasingCreditsPerToken) - .add(nonRebasingSupply); + _totalSupply = (_rebasingCredits * 1e18 / _rebasingCreditsPerToken) + + nonRebasingSupply; emit TotalSupplyUpdatedHighres( _totalSupply, @@ -698,4 +593,63 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { _rebasingCreditsPerToken ); } -} + + function delegateYield(address from, address to) external onlyGovernor nonReentrant() { + require(from != to, "Cannot delegate to self"); + require( + yieldFrom[to] == address(0) + && yieldTo[to] == address(0) + && yieldFrom[from] == address(0) + && yieldTo[from] == address(0) + , "Blocked by existing yield delegation"); + RebaseOptions stateFrom = rebaseState[from]; + RebaseOptions stateTo = rebaseState[to]; + require(_isNonRebasingAccount(from) && (stateFrom == RebaseOptions.NotSet || stateFrom == RebaseOptions.StdNonRebasing), "Must delegate from a non-rebasing account"); + require(!_isNonRebasingAccount(to) && (stateTo == RebaseOptions.NotSet || stateTo == RebaseOptions.StdRebasing), "Must delegate to a rebasing account"); + + // Set up the bidirectional links + yieldTo[from] = to; + yieldFrom[to] = from; + rebaseState[from] = RebaseOptions.YieldDelegationSource; + rebaseState[to] = RebaseOptions.YieldDelegationTarget; + + uint256 balance = balanceOf(from); + uint256 credits = _balanceToRebasingCredits(balance); + + // Local + _creditBalances[from] = balance; + alternativeCreditsPerToken[from] = 1e18; + _creditBalances[to] += credits; + + // Global + nonRebasingSupply -= balance; + _rebasingCredits += credits; + } + + function undelegateYield(address from) external onlyGovernor nonReentrant() { + // Require a delegation, which will also ensure a vaild delegation + require(yieldTo[from] != address(0), ""); + + address to = yieldTo[from]; + uint256 fromBalance = balanceOf(from); + uint256 toBalance = balanceOf(to); + uint256 toCreditsBefore = _creditBalances[to]; + uint256 toNewCredits = _balanceToRebasingCredits(toBalance); + + // Remove the bidirectional links + yieldFrom[yieldTo[from]] = address(0); + yieldTo[from] = address(0); + rebaseState[from] = RebaseOptions.StdNonRebasing; + rebaseState[to] = RebaseOptions.StdRebasing; + + // Local + _creditBalances[from] = fromBalance; + alternativeCreditsPerToken[from] = 1e18; + _creditBalances[to] = toNewCredits; + alternativeCreditsPerToken[to] = 0; // Should be not be needed + + // Global + nonRebasingSupply += fromBalance; + _rebasingCredits -= (toCreditsBefore - toNewCredits); // Should always go down or stay the same + } +} \ No newline at end of file From 99942633da96f9ccce81b67074366bd11ab55d97 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 7 Nov 2024 16:26:17 +1100 Subject: [PATCH 007/110] prettier and linter fixes --- contracts/contracts/flipper/Flipper.sol | 20 ++- contracts/contracts/token/OETH.sol | 6 +- contracts/contracts/token/OETHBase.sol | 6 +- contracts/contracts/token/OUSD.sol | 218 +++++++++++++++--------- contracts/test/token/ousd.js | 9 +- 5 files changed, 167 insertions(+), 92 deletions(-) diff --git a/contracts/contracts/flipper/Flipper.sol b/contracts/contracts/flipper/Flipper.sol index 04592d0bfb..11a71bd575 100644 --- a/contracts/contracts/flipper/Flipper.sol +++ b/contracts/contracts/flipper/Flipper.sol @@ -54,7 +54,10 @@ contract Flipper is Governable { dai.transferFrom(msg.sender, address(this), amount), "DAI transfer failed" ); - require(IERC20(ousd).transfer(msg.sender, amount), "OUSD transfer failed"); + require( + IERC20(ousd).transfer(msg.sender, amount), + "OUSD transfer failed" + ); } /// @notice Sell OUSD for Dai @@ -77,7 +80,10 @@ contract Flipper is Governable { usdc.transferFrom(msg.sender, address(this), amount / 1e12), "USDC transfer failed" ); - require(IERC20(ousd).transfer(msg.sender, amount), "OUSD transfer failed"); + require( + IERC20(ousd).transfer(msg.sender, amount), + "OUSD transfer failed" + ); } /// @notice Sell OUSD for USDC @@ -102,7 +108,10 @@ contract Flipper is Governable { // USDT does not return a boolean and reverts, // so no need for a require. usdt.transferFrom(msg.sender, address(this), amount / 1e12); - require(IERC20(ousd).transfer(msg.sender, amount), "OUSD transfer failed"); + require( + IERC20(ousd).transfer(msg.sender, amount), + "OUSD transfer failed" + ); } /// @notice Sell OUSD for USDT @@ -142,7 +151,10 @@ contract Flipper is Governable { /// again by transferring assets to the contract. function withdrawAll() external onlyGovernor nonReentrant { IERC20(dai).safeTransfer(_governor(), dai.balanceOf(address(this))); - IERC20(ousd).safeTransfer(_governor(), IERC20(ousd).balanceOf(address(this))); + IERC20(ousd).safeTransfer( + _governor(), + IERC20(ousd).balanceOf(address(this)) + ); IERC20(address(usdt)).safeTransfer( _governor(), usdt.balanceOf(address(this)) diff --git a/contracts/contracts/token/OETH.sol b/contracts/contracts/token/OETH.sol index 85f9572769..dbd491fbbb 100644 --- a/contracts/contracts/token/OETH.sol +++ b/contracts/contracts/token/OETH.sol @@ -8,15 +8,15 @@ import { OUSD } from "./OUSD.sol"; * @author Origin Protocol Inc */ contract OETH is OUSD { - function symbol() external override pure returns (string memory) { + function symbol() external pure override returns (string memory) { return "OETH"; } - function name() external override pure returns (string memory) { + function name() external pure override returns (string memory) { return "Origin Ether"; } - function decimals() external override pure returns (uint8) { + function decimals() external pure override returns (uint8) { return 18; } } diff --git a/contracts/contracts/token/OETHBase.sol b/contracts/contracts/token/OETHBase.sol index 91419f0d92..9f0c6d1b57 100644 --- a/contracts/contracts/token/OETHBase.sol +++ b/contracts/contracts/token/OETHBase.sol @@ -8,15 +8,15 @@ import { OUSD } from "./OUSD.sol"; * @author Origin Protocol Inc */ contract OETHBase is OUSD { - function symbol() external override pure returns (string memory) { + function symbol() external pure override returns (string memory) { return "superOETHb"; } - function name() external override pure returns (string memory) { + function name() external pure override returns (string memory) { return "Super OETH"; } - function decimals() external override pure returns (uint8) { + function decimals() external pure override returns (uint8) { return 18; } } diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index a600b3d53b..cb7dcef3db 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/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"; /** @@ -17,7 +18,6 @@ import { Governable } from "../governance/Governable.sol"; */ contract OUSD is Governable { - event TotalSupplyUpdatedHighres( uint256 totalSupply, uint256 rebasingCredits, @@ -26,7 +26,11 @@ contract OUSD is Governable { event AccountRebasingEnabled(address account); event AccountRebasingDisabled(address account); event Transfer(address indexed from, address indexed to, uint256 value); - event Approval(address indexed owner, address indexed spender, uint256 value); + event Approval( + address indexed owner, + address indexed spender, + uint256 value + ); enum RebaseOptions { NotSet, @@ -43,7 +47,7 @@ contract OUSD is Governable { mapping(address => uint256) private _creditBalances; uint256 private _rebasingCredits; // Sum of all rebasing credits (_creditBalances for rebasing accounts) uint256 private _rebasingCreditsPerToken; - uint256 public nonRebasingSupply; // All nonrebasing balances + uint256 public nonRebasingSupply; // All nonrebasing balances mapping(address => uint256) private alternativeCreditsPerToken; mapping(address => RebaseOptions) public rebaseState; mapping(address => uint256) public isUpgraded; @@ -64,15 +68,15 @@ contract OUSD is Governable { vaultAddress = _vaultAddress; } - function symbol() external virtual pure returns (string memory) { + function symbol() external pure virtual returns (string memory) { return "OUSD"; } - function name() external virtual pure returns (string memory) { + function name() external pure virtual returns (string memory) { return "Origin Dollar"; } - function decimals() external virtual pure returns (uint8) { + function decimals() external pure virtual returns (uint8) { return 18; } @@ -125,18 +129,15 @@ contract OUSD is Governable { * @return A uint256 representing the amount of base units owned by the * specified address. */ - function balanceOf(address _account) - public - view - returns (uint256) - { + function balanceOf(address _account) public view returns (uint256) { RebaseOptions state = rebaseState[_account]; - if(state == RebaseOptions.YieldDelegationSource){ + if (state == RebaseOptions.YieldDelegationSource) { // Saves a slot read when transfering to or from a yield delegating source // since we know creditBalances equals the balance. return _creditBalances[_account]; } - uint256 baseBalance = _creditBalances[_account] * 1e18 / _creditsPerToken(_account); + uint256 baseBalance = (_creditBalances[_account] * 1e18) / + _creditsPerToken(_account); if (state == RebaseOptions.YieldDelegationTarget) { return baseBalance - _creditBalances[yieldFrom[_account]]; } @@ -192,7 +193,11 @@ contract OUSD is Governable { } // Backwards compatible view - function nonRebasingCreditsPerToken(address _account) external view returns (uint256) { + function nonRebasingCreditsPerToken(address _account) + external + view + returns (uint256) + { return alternativeCreditsPerToken[_account]; } @@ -202,10 +207,7 @@ contract OUSD is Governable { * @param _value the amount to be transferred. * @return true on success. */ - function transfer(address _to, uint256 _value) - public - returns (bool) - { + function transfer(address _to, uint256 _value) public returns (bool) { require(_to != address(0), "Transfer to zero address"); _executeTransfer(msg.sender, _to, _value); @@ -228,7 +230,9 @@ contract OUSD is Governable { ) public returns (bool) { require(_to != address(0), "Transfer to zero address"); - _allowances[_from][msg.sender] = _allowances[_from][msg.sender] - _value; + _allowances[_from][msg.sender] = + _allowances[_from][msg.sender] - + _value; _executeTransfer(_from, _to, _value); @@ -248,14 +252,18 @@ contract OUSD is Governable { address _to, uint256 _value ) internal { - if(_from == _to){ + if (_from == _to) { return; } - (int256 fromRebasingCreditsDiff, int256 fromNonRebasingSupplyDiff) - = _adjustAccount(_from, -int256(_value)); - (int256 toRebasingCreditsDiff, int256 toNonRebasingSupplyDiff) - = _adjustAccount(_to, int256(_value)); + ( + int256 fromRebasingCreditsDiff, + int256 fromNonRebasingSupplyDiff + ) = _adjustAccount(_from, -int256(_value)); + ( + int256 toRebasingCreditsDiff, + int256 toNonRebasingSupplyDiff + ) = _adjustAccount(_to, int256(_value)); _adjustGlobals( fromRebasingCreditsDiff + toRebasingCreditsDiff, @@ -263,52 +271,71 @@ contract OUSD is Governable { ); } - function _adjustAccount(address account, int256 balanceChange) internal returns (int256 rebasingCreditsDiff, int256 nonRebasingSupplyDiff) { + function _adjustAccount(address account, int256 balanceChange) + internal + returns (int256 rebasingCreditsDiff, int256 nonRebasingSupplyDiff) + { RebaseOptions state = rebaseState[account]; int256 currentBalance = int256(balanceOf(account)); - uint256 newBalance = uint256(int256(currentBalance) + int256(balanceChange)); - if(newBalance < 0){ + uint256 newBalance = uint256( + int256(currentBalance) + int256(balanceChange) + ); + if (newBalance < 0) { revert("Transfer amount exceeds balance"); } if (state == RebaseOptions.YieldDelegationSource) { address target = yieldTo[account]; uint256 targetOldBalance = balanceOf(target); - uint256 targetNewCredits = _balanceToRebasingCredits(targetOldBalance + newBalance); - rebasingCreditsDiff = int256(targetNewCredits) - int256(_creditBalances[target]); + uint256 targetNewCredits = _balanceToRebasingCredits( + targetOldBalance + newBalance + ); + rebasingCreditsDiff = + int256(targetNewCredits) - + int256(_creditBalances[target]); _creditBalances[account] = newBalance; _creditBalances[target] = targetNewCredits; alternativeCreditsPerToken[account] = 1e18; - } else if (state == RebaseOptions.YieldDelegationTarget) { - uint256 newCredits = _balanceToRebasingCredits(newBalance + _creditBalances[yieldFrom[account]]); - rebasingCreditsDiff = int256(newCredits) - int256(_creditBalances[account]); + uint256 newCredits = _balanceToRebasingCredits( + newBalance + _creditBalances[yieldFrom[account]] + ); + rebasingCreditsDiff = + int256(newCredits) - + int256(_creditBalances[account]); _creditBalances[account] = newCredits; - - } else if(_isNonRebasingAccount(account)){ + } else if (_isNonRebasingAccount(account)) { nonRebasingSupplyDiff = balanceChange; alternativeCreditsPerToken[account] = 1e18; _creditBalances[account] = newBalance; - } else { uint256 newCredits = _balanceToRebasingCredits(newBalance); - rebasingCreditsDiff = int256(newCredits) - int256(_creditBalances[account]); + rebasingCreditsDiff = + int256(newCredits) - + int256(_creditBalances[account]); _creditBalances[account] = newCredits; } } - function _adjustGlobals(int256 rebasingCreditsDiff, int256 nonRebasingSupplyDiff) internal { - if(rebasingCreditsDiff !=0){ - if (uint256(int256(_rebasingCredits) + rebasingCreditsDiff) < 0){ + function _adjustGlobals( + int256 rebasingCreditsDiff, + int256 nonRebasingSupplyDiff + ) internal { + if (rebasingCreditsDiff != 0) { + if (uint256(int256(_rebasingCredits) + rebasingCreditsDiff) < 0) { revert("rebasingCredits underflow"); } - _rebasingCredits = uint256(int256(_rebasingCredits) + rebasingCreditsDiff); + _rebasingCredits = uint256( + int256(_rebasingCredits) + rebasingCreditsDiff + ); } - if(nonRebasingSupplyDiff !=0){ - if (int256(nonRebasingSupply) + nonRebasingSupplyDiff < 0){ + if (nonRebasingSupplyDiff != 0) { + if (int256(nonRebasingSupply) + nonRebasingSupplyDiff < 0) { revert("nonRebasingSupply underflow"); } - nonRebasingSupply = uint256(int256(nonRebasingSupply) + nonRebasingSupplyDiff); + nonRebasingSupply = uint256( + int256(nonRebasingSupply) + nonRebasingSupplyDiff + ); } } @@ -333,10 +360,7 @@ contract OUSD is Governable { * @param _spender The address which will spend the funds. * @param _value The amount of tokens to be spent. */ - function approve(address _spender, uint256 _value) - public - returns (bool) - { + function approve(address _spender, uint256 _value) public returns (bool) { _allowances[msg.sender][_spender] = _value; emit Approval(msg.sender, _spender, _value); return true; @@ -354,7 +378,8 @@ contract OUSD is Governable { public returns (bool) { - uint256 updatedAllowance = _allowances[msg.sender][_spender] + _addedValue; + uint256 updatedAllowance = _allowances[msg.sender][_spender] + + _addedValue; _allowances[msg.sender][_spender] = updatedAllowance; emit Approval(msg.sender, _spender, updatedAllowance); return true; @@ -404,8 +429,10 @@ contract OUSD is Governable { require(_account != address(0), "Mint to the zero address"); // Account - (int256 toRebasingCreditsDiff, int256 toNonRebasingSupplyDiff) - = _adjustAccount(_account, int256(_amount)); + ( + int256 toRebasingCreditsDiff, + int256 toNonRebasingSupplyDiff + ) = _adjustAccount(_account, int256(_amount)); // Globals _adjustGlobals(toRebasingCreditsDiff, toNonRebasingSupplyDiff); _totalSupply = _totalSupply + _amount; @@ -439,8 +466,10 @@ contract OUSD is Governable { } // Account - (int256 toRebasingCreditsDiff, int256 toNonRebasingSupplyDiff) - = _adjustAccount(_account, -int256(_amount)); + ( + int256 toRebasingCreditsDiff, + int256 toNonRebasingSupplyDiff + ) = _adjustAccount(_account, -int256(_amount)); // Globals _adjustGlobals(toRebasingCreditsDiff, toNonRebasingSupplyDiff); _totalSupply = _totalSupply - _amount; @@ -472,13 +501,21 @@ contract OUSD is Governable { */ function _isNonRebasingAccount(address _account) internal returns (bool) { bool isContract = _account.code.length > 0; - if (isContract && rebaseState[_account] == RebaseOptions.NotSet && alternativeCreditsPerToken[_account] != 0) { + if ( + isContract && + rebaseState[_account] == RebaseOptions.NotSet && + alternativeCreditsPerToken[_account] != 0 + ) { _rebaseOptOut(msg.sender); } return alternativeCreditsPerToken[_account] > 0; } - function _balanceToRebasingCredits(uint256 balance) internal view returns (uint256) { + function _balanceToRebasingCredits(uint256 balance) + internal + view + returns (uint256) + { // Rounds up, because we need to ensure that accounts allways have // at least the balance that they should have. // Note this should always be used on an absolute account value, @@ -514,10 +551,14 @@ contract OUSD is Governable { // TODO below line fails when deploying core001 //require(alternativeCreditsPerToken[_account] != 0, "Account must be non-rebasing"); RebaseOptions state = rebaseState[_account]; - require(state == RebaseOptions.StdNonRebasing || state == RebaseOptions.NotSet, "Only standard non-rebasing accounts can opt out"); + require( + state == RebaseOptions.StdNonRebasing || + state == RebaseOptions.NotSet, + "Only standard non-rebasing accounts can opt out" + ); uint256 balance = balanceOf(msg.sender); - + // Account rebaseState[msg.sender] = RebaseOptions.StdRebasing; alternativeCreditsPerToken[msg.sender] = 0; @@ -535,13 +576,19 @@ contract OUSD is Governable { } function _rebaseOptOut(address _account) internal { - require(alternativeCreditsPerToken[_account] == 0, "Account must be rebasing"); + require( + alternativeCreditsPerToken[_account] == 0, + "Account must be rebasing" + ); RebaseOptions state = rebaseState[_account]; - require(state == RebaseOptions.StdRebasing || state == RebaseOptions.NotSet, "Only standard rebasing accounts can opt out"); - + require( + state == RebaseOptions.StdRebasing || state == RebaseOptions.NotSet, + "Only standard rebasing accounts can opt out" + ); + uint256 oldCredits = _creditBalances[_account]; uint256 balance = balanceOf(_account); - + // Account rebaseState[_account] = RebaseOptions.StdNonRebasing; alternativeCreditsPerToken[_account] = 1e18; @@ -579,13 +626,15 @@ contract OUSD is Governable { ? MAX_SUPPLY : _newTotalSupply; - _rebasingCreditsPerToken = _rebasingCredits - * 1e18 / (_totalSupply - nonRebasingSupply); + _rebasingCreditsPerToken = + (_rebasingCredits * 1e18) / + (_totalSupply - nonRebasingSupply); require(_rebasingCreditsPerToken > 0, "Invalid change in supply"); - _totalSupply = (_rebasingCredits * 1e18 / _rebasingCreditsPerToken) - + nonRebasingSupply; + _totalSupply = + ((_rebasingCredits * 1e18) / _rebasingCreditsPerToken) + + nonRebasingSupply; emit TotalSupplyUpdatedHighres( _totalSupply, @@ -594,19 +643,34 @@ contract OUSD is Governable { ); } - function delegateYield(address from, address to) external onlyGovernor nonReentrant() { + function delegateYield(address from, address to) + external + onlyGovernor + nonReentrant + { require(from != to, "Cannot delegate to self"); require( - yieldFrom[to] == address(0) - && yieldTo[to] == address(0) - && yieldFrom[from] == address(0) - && yieldTo[from] == address(0) - , "Blocked by existing yield delegation"); + yieldFrom[to] == address(0) && + yieldTo[to] == address(0) && + yieldFrom[from] == address(0) && + yieldTo[from] == address(0), + "Blocked by existing yield delegation" + ); RebaseOptions stateFrom = rebaseState[from]; RebaseOptions stateTo = rebaseState[to]; - require(_isNonRebasingAccount(from) && (stateFrom == RebaseOptions.NotSet || stateFrom == RebaseOptions.StdNonRebasing), "Must delegate from a non-rebasing account"); - require(!_isNonRebasingAccount(to) && (stateTo == RebaseOptions.NotSet || stateTo == RebaseOptions.StdRebasing), "Must delegate to a rebasing account"); - + require( + _isNonRebasingAccount(from) && + (stateFrom == RebaseOptions.NotSet || + stateFrom == RebaseOptions.StdNonRebasing), + "Must delegate from a non-rebasing account" + ); + require( + !_isNonRebasingAccount(to) && + (stateTo == RebaseOptions.NotSet || + stateTo == RebaseOptions.StdRebasing), + "Must delegate to a rebasing account" + ); + // Set up the bidirectional links yieldTo[from] = to; yieldFrom[to] = from; @@ -626,16 +690,16 @@ contract OUSD is Governable { _rebasingCredits += credits; } - function undelegateYield(address from) external onlyGovernor nonReentrant() { + function undelegateYield(address from) external onlyGovernor nonReentrant { // Require a delegation, which will also ensure a vaild delegation - require(yieldTo[from] != address(0), ""); + require(yieldTo[from] != address(0), ""); address to = yieldTo[from]; uint256 fromBalance = balanceOf(from); uint256 toBalance = balanceOf(to); uint256 toCreditsBefore = _creditBalances[to]; uint256 toNewCredits = _balanceToRebasingCredits(toBalance); - + // Remove the bidirectional links yieldFrom[yieldTo[from]] = address(0); yieldTo[from] = address(0); @@ -652,4 +716,4 @@ contract OUSD is Governable { nonRebasingSupply += fromBalance; _rebasingCredits -= (toCreditsBefore - toNewCredits); // Should always go down or stay the same } -} \ No newline at end of file +} diff --git a/contracts/test/token/ousd.js b/contracts/test/token/ousd.js index f3ecc01109..8b7cde82a0 100644 --- a/contracts/test/token/ousd.js +++ b/contracts/test/token/ousd.js @@ -4,7 +4,7 @@ const { utils } = require("ethers"); const { daiUnits, ousdUnits, usdcUnits, isFork } = require("../helpers"); -describe.only("Token", function () { +describe("Token", function () { if (isFork) { this.timeout(0); } @@ -853,7 +853,7 @@ describe.only("Token", function () { describe("Delegating yield", function () { // TODO delete below test later it("Figure out gas costs", async () => { - let { ousd, vault, matt, josh, anna, usdc, governor } = fixture; + let { ousd, matt, josh, anna, governor } = fixture; await expect(josh).has.an.approxBalanceOf("100", ousd); await expect(matt).has.an.approxBalanceOf("100", ousd); @@ -869,9 +869,8 @@ describe.only("Token", function () { await ousd.connect(matt).transfer(josh.address, ousdUnits("2")); await ousd.connect(matt).transfer(josh.address, ousdUnits("2")); - }); - + it("Should delegate rebase to another account", async () => { let { ousd, vault, matt, josh, anna, usdc, governor } = fixture; @@ -905,7 +904,7 @@ describe.only("Token", function () { console.log("Matt transfering to josh"); await ousd.connect(matt).transfer(josh.address, ousdUnits("80")); console.log("Anna transfering to josh"); - await ousd.connect(anna).transfer(josh.address, ousdUnits("90")) + await ousd.connect(anna).transfer(josh.address, ousdUnits("90")); await expect(josh).has.an.approxBalanceOf("400", ousd); await expect(matt).has.an.approxBalanceOf("0", ousd); From 4c22467dbbcad8e081d18a942cbae8e06f599ed3 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 7 Nov 2024 16:27:08 +1100 Subject: [PATCH 008/110] Add slots to OUSD contract to align with existing deployments --- contracts/contracts/token/OUSD.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index cb7dcef3db..e85b2422ad 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -40,6 +40,8 @@ contract OUSD is Governable { YieldDelegationTarget } + // Add slots to align with deployed OUSD contract + uint256[154] private _gap; uint256 private constant MAX_SUPPLY = ~uint128(0); // (2^128) - 1 uint256 public _totalSupply; mapping(address => mapping(address => uint256)) private _allowances; From f537b6347dcba5cf7fa65f8eea09b493e457b777 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 7 Nov 2024 16:27:25 +1100 Subject: [PATCH 009/110] Generated new OUSD contract diagrams --- contracts/docs/OUSDHierarchy.svg | 78 +++---------- contracts/docs/OUSDSquashed.svg | 178 +++++++++++++++--------------- contracts/docs/OUSDStorage.svg | 184 +++++++++++++++++-------------- 3 files changed, 210 insertions(+), 230 deletions(-) diff --git a/contracts/docs/OUSDHierarchy.svg b/contracts/docs/OUSDHierarchy.svg index 69256b4124..48065cc6b3 100644 --- a/contracts/docs/OUSDHierarchy.svg +++ b/contracts/docs/OUSDHierarchy.svg @@ -4,72 +4,30 @@ - - + + UmlClassDiagram - - + + -20 - -Governable -../contracts/governance/Governable.sol +21 + +Governable +../contracts/governance/Governable.sol - + -186 - -OUSD -../contracts/token/OUSD.sol +228 + +OUSD +../contracts/token/OUSD.sol - - -186->20 - - - - - -194 - -<<Abstract>> -Initializable -../contracts/utils/Initializable.sol - - + -186->194 - - - - - -197 - -<<Abstract>> -InitializableERC20Detailed -../contracts/utils/InitializableERC20Detailed.sol - - - -186->197 - - - - - -392 - -<<Interface>> -IERC20 -../node_modules/@openzeppelin/contracts/token/ERC20/IERC20.sol - - - -197->392 - - +228->21 + + diff --git a/contracts/docs/OUSDSquashed.svg b/contracts/docs/OUSDSquashed.svg index bf4f7032c0..0da5b4771a 100644 --- a/contracts/docs/OUSDSquashed.svg +++ b/contracts/docs/OUSDSquashed.svg @@ -4,95 +4,99 @@ - - + + UmlClassDiagram - - + + -186 - -OUSD -../contracts/token/OUSD.sol - -Private: -   initialized: bool <<Initializable>> -   initializing: bool <<Initializable>> -   ______gap: uint256[50] <<Initializable>> -   _____gap: uint256[100] <<InitializableERC20Detailed>> -   _name: string <<InitializableERC20Detailed>> -   _symbol: string <<InitializableERC20Detailed>> -   _decimals: uint8 <<InitializableERC20Detailed>> -   governorPosition: bytes32 <<Governable>> -   pendingGovernorPosition: bytes32 <<Governable>> -   reentryStatusPosition: bytes32 <<Governable>> -   MAX_SUPPLY: uint256 <<OUSD>> -   _allowances: mapping(address=>mapping(address=>uint256)) <<OUSD>> -   _creditBalances: mapping(address=>uint256) <<OUSD>> -   _rebasingCredits: uint256 <<OUSD>> -   _rebasingCreditsPerToken: uint256 <<OUSD>> -   RESOLUTION_INCREASE: uint256 <<OUSD>> -Public: -   _NOT_ENTERED: uint256 <<Governable>> -   _ENTERED: uint256 <<Governable>> -   _totalSupply: uint256 <<OUSD>> -   vaultAddress: address <<OUSD>> -   nonRebasingSupply: uint256 <<OUSD>> -   nonRebasingCreditsPerToken: mapping(address=>uint256) <<OUSD>> -   rebaseState: mapping(address=>RebaseOptions) <<OUSD>> -   isUpgraded: mapping(address=>uint256) <<OUSD>> - -Internal: -    _initialize(nameArg: string, symbolArg: string, decimalsArg: uint8) <<InitializableERC20Detailed>> -    _governor(): (governorOut: address) <<Governable>> -    _pendingGovernor(): (pendingGovernor: address) <<Governable>> -    _setGovernor(newGovernor: address) <<Governable>> -    _setPendingGovernor(newGovernor: address) <<Governable>> -    _changeGovernor(_newGovernor: address) <<Governable>> -    _executeTransfer(_from: address, _to: address, _value: uint256) <<OUSD>> -    _mint(_account: address, _amount: uint256) <<nonReentrant>> <<OUSD>> -    _burn(_account: address, _amount: uint256) <<nonReentrant>> <<OUSD>> -    _creditsPerToken(_account: address): uint256 <<OUSD>> -    _isNonRebasingAccount(_account: address): bool <<OUSD>> -    _ensureRebasingMigration(_account: address) <<OUSD>> -External: -    transferGovernance(_newGovernor: address) <<onlyGovernor>> <<Governable>> -    claimGovernance() <<Governable>> -    initialize(_nameArg: string, _symbolArg: string, _vaultAddress: address, _initialCreditsPerToken: uint256) <<onlyGovernor, initializer>> <<OUSD>> -    mint(_account: address, _amount: uint256) <<onlyVault>> <<OUSD>> -    burn(account: address, amount: uint256) <<onlyVault>> <<OUSD>> -    changeSupply(_newTotalSupply: uint256) <<onlyVault, nonReentrant>> <<OUSD>> -Public: -    <<event>> Transfer(from: address, to: address, value: uint256) <<IERC20>> -    <<event>> Approval(owner: address, spender: address, value: uint256) <<IERC20>> -    <<event>> PendingGovernorshipTransfer(previousGovernor: address, newGovernor: address) <<Governable>> -    <<event>> GovernorshipTransferred(previousGovernor: address, newGovernor: address) <<Governable>> -    <<event>> TotalSupplyUpdatedHighres(totalSupply: uint256, rebasingCredits: uint256, rebasingCreditsPerToken: uint256) <<OUSD>> -    <<modifier>> initializer() <<Initializable>> -    <<modifier>> onlyGovernor() <<Governable>> -    <<modifier>> nonReentrant() <<Governable>> -    <<modifier>> onlyVault() <<OUSD>> -    totalSupply(): uint256 <<OUSD>> -    balanceOf(_account: address): uint256 <<OUSD>> -    transfer(_to: address, _value: uint256): bool <<OUSD>> -    allowance(_owner: address, _spender: address): uint256 <<OUSD>> -    approve(_spender: address, _value: uint256): bool <<OUSD>> -    transferFrom(_from: address, _to: address, _value: uint256): bool <<OUSD>> -    name(): string <<InitializableERC20Detailed>> -    symbol(): string <<InitializableERC20Detailed>> -    decimals(): uint8 <<InitializableERC20Detailed>> -    constructor() <<Governable>> -    governor(): address <<Governable>> -    isGovernor(): bool <<Governable>> -    rebasingCreditsPerToken(): uint256 <<OUSD>> -    rebasingCredits(): uint256 <<OUSD>> -    rebasingCreditsPerTokenHighres(): uint256 <<OUSD>> -    rebasingCreditsHighres(): uint256 <<OUSD>> -    creditsBalanceOf(_account: address): (uint256, uint256) <<OUSD>> -    creditsBalanceOfHighres(_account: address): (uint256, uint256, bool) <<OUSD>> -    increaseAllowance(_spender: address, _addedValue: uint256): bool <<OUSD>> -    decreaseAllowance(_spender: address, _subtractedValue: uint256): bool <<OUSD>> +228 + +OUSD +../contracts/token/OUSD.sol + +Private: +   governorPosition: bytes32 <<Governable>> +   pendingGovernorPosition: bytes32 <<Governable>> +   reentryStatusPosition: bytes32 <<Governable>> +   _gap: uint256[154] <<OUSD>> +   MAX_SUPPLY: uint256 <<OUSD>> +   _allowances: mapping(address=>mapping(address=>uint256)) <<OUSD>> +   _creditBalances: mapping(address=>uint256) <<OUSD>> +   _rebasingCredits: uint256 <<OUSD>> +   _rebasingCreditsPerToken: uint256 <<OUSD>> +   alternativeCreditsPerToken: mapping(address=>uint256) <<OUSD>> +   RESOLUTION_INCREASE: uint256 <<OUSD>> +Public: +   _NOT_ENTERED: uint256 <<Governable>> +   _ENTERED: uint256 <<Governable>> +   _totalSupply: uint256 <<OUSD>> +   vaultAddress: address <<OUSD>> +   nonRebasingSupply: uint256 <<OUSD>> +   rebaseState: mapping(address=>RebaseOptions) <<OUSD>> +   isUpgraded: mapping(address=>uint256) <<OUSD>> +   yieldTo: mapping(address=>address) <<OUSD>> +   yieldFrom: mapping(address=>address) <<OUSD>> + +Internal: +    _governor(): (governorOut: address) <<Governable>> +    _pendingGovernor(): (pendingGovernor: address) <<Governable>> +    _setGovernor(newGovernor: address) <<Governable>> +    _setPendingGovernor(newGovernor: address) <<Governable>> +    _changeGovernor(_newGovernor: address) <<Governable>> +    _executeTransfer(_from: address, _to: address, _value: uint256) <<OUSD>> +    _adjustAccount(account: address, balanceChange: int256): (rebasingCreditsDiff: int256, nonRebasingSupplyDiff: int256) <<OUSD>> +    _adjustGlobals(rebasingCreditsDiff: int256, nonRebasingSupplyDiff: int256) <<OUSD>> +    _mint(_account: address, _amount: uint256) <<nonReentrant>> <<OUSD>> +    _burn(_account: address, _amount: uint256) <<nonReentrant>> <<OUSD>> +    _creditsPerToken(_account: address): uint256 <<OUSD>> +    _isNonRebasingAccount(_account: address): bool <<OUSD>> +    _balanceToRebasingCredits(balance: uint256): uint256 <<OUSD>> +    _rebaseOptIn(_account: address) <<OUSD>> +    _rebaseOptOut(_account: address) <<OUSD>> +External: +    transferGovernance(_newGovernor: address) <<onlyGovernor>> <<Governable>> +    claimGovernance() <<Governable>> +    initialize(string, string, _vaultAddress: address, _initialCreditsPerToken: uint256) <<onlyGovernor>> <<OUSD>> +    symbol(): string <<OUSD>> +    name(): string <<OUSD>> +    decimals(): uint8 <<OUSD>> +    nonRebasingCreditsPerToken(_account: address): uint256 <<OUSD>> +    mint(_account: address, _amount: uint256) <<onlyVault>> <<OUSD>> +    burn(account: address, amount: uint256) <<onlyVault>> <<OUSD>> +    changeSupply(_newTotalSupply: uint256) <<onlyVault, nonReentrant>> <<OUSD>> +    delegateYield(from: address, to: address) <<onlyGovernor, nonReentrant>> <<OUSD>> +    undelegateYield(from: address) <<onlyGovernor, nonReentrant>> <<OUSD>> +Public: +    <<event>> PendingGovernorshipTransfer(previousGovernor: address, newGovernor: address) <<Governable>> +    <<event>> GovernorshipTransferred(previousGovernor: address, newGovernor: address) <<Governable>> +    <<event>> TotalSupplyUpdatedHighres(totalSupply: uint256, rebasingCredits: uint256, rebasingCreditsPerToken: uint256) <<OUSD>> +    <<event>> AccountRebasingEnabled(account: address) <<OUSD>> +    <<event>> AccountRebasingDisabled(account: address) <<OUSD>> +    <<event>> Transfer(from: address, to: address, value: uint256) <<OUSD>> +    <<event>> Approval(owner: address, spender: address, value: uint256) <<OUSD>> +    <<modifier>> onlyGovernor() <<Governable>> +    <<modifier>> nonReentrant() <<Governable>> +    <<modifier>> onlyVault() <<OUSD>> +    constructor() <<Governable>> +    governor(): address <<Governable>> +    isGovernor(): bool <<Governable>> +    totalSupply(): uint256 <<OUSD>> +    rebasingCreditsPerTokenHighres(): uint256 <<OUSD>> +    rebasingCreditsPerToken(): uint256 <<OUSD>> +    rebasingCreditsHighres(): uint256 <<OUSD>> +    rebasingCredits(): uint256 <<OUSD>> +    balanceOf(_account: address): uint256 <<OUSD>> +    creditsBalanceOf(_account: address): (uint256, uint256) <<OUSD>> +    creditsBalanceOfHighres(_account: address): (uint256, uint256, bool) <<OUSD>> +    transfer(_to: address, _value: uint256): bool <<OUSD>> +    transferFrom(_from: address, _to: address, _value: uint256): bool <<OUSD>> +    allowance(_owner: address, _spender: address): uint256 <<OUSD>> +    approve(_spender: address, _value: uint256): bool <<OUSD>> +    increaseAllowance(_spender: address, _addedValue: uint256): bool <<OUSD>> +    decreaseAllowance(_spender: address, _subtractedValue: uint256): bool <<OUSD>> +    governanceRebaseOptIn(_account: address) <<nonReentrant, onlyGovernor>> <<OUSD>>    rebaseOptIn() <<nonReentrant>> <<OUSD>>    rebaseOptOut() <<nonReentrant>> <<OUSD>> diff --git a/contracts/docs/OUSDStorage.svg b/contracts/docs/OUSDStorage.svg index d166562322..e26f92aad3 100644 --- a/contracts/docs/OUSDStorage.svg +++ b/contracts/docs/OUSDStorage.svg @@ -4,92 +4,110 @@ - - + + StorageDiagram - - + + +2 + +OUSD <<Contract>> + +slot + +0-153 + +154 + +155 + +156 + +157 + +158 + +159 + +160 + +161 + +162 + +163 + +164 + +165 + +type: <inherited contract>.variable (bytes) + +uint256[154]: _gap (4928) + +uint256: _totalSupply (32) + +mapping(address=>mapping(address=>uint256)): _allowances (32) + +unallocated (12) + +address: vaultAddress (20) + +mapping(address=>uint256): _creditBalances (32) + +uint256: _rebasingCredits (32) + +uint256: _rebasingCreditsPerToken (32) + +uint256: nonRebasingSupply (32) + +mapping(address=>uint256): alternativeCreditsPerToken (32) + +mapping(address=>RebaseOptions): rebaseState (32) + +mapping(address=>uint256): isUpgraded (32) + +mapping(address=>address): yieldTo (32) + +mapping(address=>address): yieldFrom (32) + + + 1 - -OUSD <<Contract>> - -slot - -0 - -1-50 - -51-150 - -151 - -152 - -153 - -154 - -155 - -156 - -157 - -158 - -159 - -160 - -161 - -162 - -163 - -type: <inherited contract>.variable (bytes) - -unallocated (30) - -bool: Initializable.initializing (1) - -bool: Initializable.initialized (1) - -uint256[50]: Initializable.______gap (1600) - -uint256[100]: InitializableERC20Detailed._____gap (3200) - -string: InitializableERC20Detailed._name (32) - -string: InitializableERC20Detailed._symbol (32) - -unallocated (31) - -uint8: InitializableERC20Detailed._decimals (1) - -uint256: _totalSupply (32) - -mapping(address=>mapping(address=>uint256)): _allowances (32) - -unallocated (12) - -address: vaultAddress (20) - -mapping(address=>uint256): _creditBalances (32) - -uint256: _rebasingCredits (32) - -uint256: _rebasingCreditsPerToken (32) - -uint256: nonRebasingSupply (32) - -mapping(address=>uint256): nonRebasingCreditsPerToken (32) - -mapping(address=>RebaseOptions): rebaseState (32) - -mapping(address=>uint256): isUpgraded (32) + +uint256[154]: _gap <<Array>> + +slot + +0 + +1 + +2-151 + +152 + +153 + +type: variable (bytes) + +uint256 (32) + +uint256 (32) + +---- (4800) + +uint256 (32) + +uint256 (32) + + + +2:6->1 + + From 5743461479a29423ef6373a78fd22338bf672be9 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 7 Nov 2024 18:14:01 +1100 Subject: [PATCH 010/110] Added deploy scripts for OToken upgrades --- contracts/deploy/base/021_upgrade_oeth.js | 24 +++++++++++++ contracts/deploy/mainnet/108_vault_upgrade.js | 12 ++++++- contracts/deploy/mainnet/109_ousd_upgrade.js | 34 +++++++++++++++++++ 3 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 contracts/deploy/base/021_upgrade_oeth.js create mode 100644 contracts/deploy/mainnet/109_ousd_upgrade.js diff --git a/contracts/deploy/base/021_upgrade_oeth.js b/contracts/deploy/base/021_upgrade_oeth.js new file mode 100644 index 0000000000..48a24160c6 --- /dev/null +++ b/contracts/deploy/base/021_upgrade_oeth.js @@ -0,0 +1,24 @@ +const { deployOnBaseWithGuardian } = require("../../utils/deploy-l2"); +const { deployWithConfirmation } = require("../../utils/deploy"); + +module.exports = deployOnBaseWithGuardian( + { + deployName: "021_upgrade_oeth", + }, + async ({ ethers }) => { + const dOETHb = await deployWithConfirmation("OETH", [], "OETH", true); + + const cOETHbProxy = await ethers.getContract("OETHBaseProxy"); + + return { + actions: [ + { + // 1. Upgrade OETH + contract: cOETHbProxy, + signature: "upgradeTo(address)", + args: [dOETHb.address], + }, + ], + }; + } +); diff --git a/contracts/deploy/mainnet/108_vault_upgrade.js b/contracts/deploy/mainnet/108_vault_upgrade.js index 31fefb5241..f5c3806df3 100644 --- a/contracts/deploy/mainnet/108_vault_upgrade.js +++ b/contracts/deploy/mainnet/108_vault_upgrade.js @@ -26,6 +26,10 @@ module.exports = deploymentWithGovernanceProposal( const cVaultProxy = await ethers.getContract("OETHVaultProxy"); const cVault = await ethers.getContractAt("IVault", cVaultProxy.address); + // 3. Deploy new OETH implementation without storage slot checks + const dOETH = await deployWithConfirmation("OETH", [], "OETH", true); + const cOETHProxy = await ethers.getContract("OETHProxy"); + // Governance Actions // ---------------- return { @@ -43,12 +47,18 @@ module.exports = deploymentWithGovernanceProposal( signature: "setAdminImpl(address)", args: [dVaultAdmin.address], }, + // 3. Set async claim delay to 10 minutes { - // 3. Set async claim delay to 10 minutes contract: cVault, signature: "setWithdrawalClaimDelay(uint256)", args: [10 * 60], // 10 mins }, + // 4. Upgrade the OETH proxy to the new implementation + { + contract: cOETHProxy, + signature: "upgradeTo(address)", + args: [dOETH.address], + }, ], }; } diff --git a/contracts/deploy/mainnet/109_ousd_upgrade.js b/contracts/deploy/mainnet/109_ousd_upgrade.js new file mode 100644 index 0000000000..1ae55ec011 --- /dev/null +++ b/contracts/deploy/mainnet/109_ousd_upgrade.js @@ -0,0 +1,34 @@ +const { deploymentWithGovernanceProposal } = require("../../utils/deploy"); + +module.exports = deploymentWithGovernanceProposal( + { + deployName: "109_ousd_upgrade", + forceDeploy: false, + //forceSkip: true, + reduceQueueTime: true, + deployerIsProposer: false, + proposalId: "", + }, + async ({ deployWithConfirmation }) => { + // Deployer Actions + // ---------------- + + // 1. Deploy new OUSD implementation without storage slot checks + const dOUSD = await deployWithConfirmation("OUSD", [], "OUSD", true); + const cOUSDProxy = await ethers.getContract("OUSDProxy"); + + // Governance Actions + // ---------------- + return { + name: "Upgrade OUSD token contract", + actions: [ + // 1. Upgrade the OUSD proxy to the new implementation + { + contract: cOUSDProxy, + signature: "upgradeTo(address)", + args: [dOUSD.address], + }, + ], + }; + } +); From 68ae87eefb28aa1c6ec63e919d8406589d2ac7ba Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 7 Nov 2024 18:14:34 +1100 Subject: [PATCH 011/110] Generated new OUSD storage diagram --- contracts/docs/OUSDStorage.svg | 48 +++++----------------------------- contracts/docs/generate.sh | 2 +- 2 files changed, 7 insertions(+), 43 deletions(-) diff --git a/contracts/docs/OUSDStorage.svg b/contracts/docs/OUSDStorage.svg index e26f92aad3..2adf9ca802 100644 --- a/contracts/docs/OUSDStorage.svg +++ b/contracts/docs/OUSDStorage.svg @@ -4,14 +4,14 @@ - - + + StorageDiagram - - + + -2 +1 OUSD <<Contract>> @@ -73,41 +73,5 @@ mapping(address=>address): yieldFrom (32) - - -1 - -uint256[154]: _gap <<Array>> - -slot - -0 - -1 - -2-151 - -152 - -153 - -type: variable (bytes) - -uint256 (32) - -uint256 (32) - ----- (4800) - -uint256 (32) - -uint256 (32) - - - -2:6->1 - - - diff --git a/contracts/docs/generate.sh b/contracts/docs/generate.sh index 84f751ae9a..69e89d4f0c 100644 --- a/contracts/docs/generate.sh +++ b/contracts/docs/generate.sh @@ -118,7 +118,7 @@ sol2uml storage .. -c Timelock -o TimelockStorage.svg # contracts/token sol2uml .. -v -hv -hf -he -hs -hl -b OUSD -o OUSDHierarchy.svg sol2uml .. -s -d 0 -b OUSD -o OUSDSquashed.svg -sol2uml storage .. -c OUSD -o OUSDStorage.svg --hideExpand _____gap,______gap +sol2uml storage .. -c OUSD -o OUSDStorage.svg --hideExpand _gap sol2uml .. -v -hv -hf -he -hs -hl -b WrappedOusd -o WOUSDHierarchy.svg sol2uml .. -s -d 0 -b WrappedOusd -o WOUSDSquashed.svg From 58afd12cd88d532a4e3e899cbfa464d0943a7ed7 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Thu, 7 Nov 2024 08:24:01 +0100 Subject: [PATCH 012/110] some minor bug fixes --- contracts/contracts/token/OUSD.sol | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index e85b2422ad..151f7d177c 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -9,8 +9,6 @@ pragma solidity ^0.8.0; */ 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 * balanceOf(x) for all x is not >= totalSupply(). This is a consequence of the @@ -505,10 +503,9 @@ contract OUSD is Governable { bool isContract = _account.code.length > 0; if ( isContract && - rebaseState[_account] == RebaseOptions.NotSet && - alternativeCreditsPerToken[_account] != 0 + rebaseState[_account] == RebaseOptions.NotSet ) { - _rebaseOptOut(msg.sender); + _rebaseOptOut(_account); } return alternativeCreditsPerToken[_account] > 0; } @@ -550,8 +547,7 @@ contract OUSD is Governable { } function _rebaseOptIn(address _account) internal { - // TODO below line fails when deploying core001 - //require(alternativeCreditsPerToken[_account] != 0, "Account must be non-rebasing"); + require(_isNonRebasingAccount(_account), "Account must be non-rebasing"); RebaseOptions state = rebaseState[_account]; require( state == RebaseOptions.StdNonRebasing || @@ -578,11 +574,14 @@ contract OUSD is Governable { } function _rebaseOptOut(address _account) internal { - require( - alternativeCreditsPerToken[_account] == 0, - "Account must be rebasing" - ); RebaseOptions state = rebaseState[_account]; + if (state == RebaseOptions.StdRebasing) { + require( + alternativeCreditsPerToken[_account] == 0, + "Account must be rebasing" + ); + } + require( state == RebaseOptions.StdRebasing || state == RebaseOptions.NotSet, "Only standard rebasing accounts can opt out" @@ -712,7 +711,7 @@ contract OUSD is Governable { _creditBalances[from] = fromBalance; alternativeCreditsPerToken[from] = 1e18; _creditBalances[to] = toNewCredits; - alternativeCreditsPerToken[to] = 0; // Should be not be needed + alternativeCreditsPerToken[to] = 0; // Is needed otherwise rebaseOptOut check will not pass // Global nonRebasingSupply += fromBalance; From 17f121c66b3c5d7b9052c0218c39f0637a1ff979 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 7 Nov 2024 18:43:24 +1100 Subject: [PATCH 013/110] Prettier and fix spelling in comments --- contracts/contracts/token/OUSD.sol | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 151f7d177c..4223f1bac3 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -132,7 +132,7 @@ contract OUSD is Governable { function balanceOf(address _account) public view returns (uint256) { RebaseOptions state = rebaseState[_account]; if (state == RebaseOptions.YieldDelegationSource) { - // Saves a slot read when transfering to or from a yield delegating source + // Saves a slot read when transferring to or from a yield delegating source // since we know creditBalances equals the balance. return _creditBalances[_account]; } @@ -501,10 +501,7 @@ contract OUSD is Governable { */ function _isNonRebasingAccount(address _account) internal returns (bool) { bool isContract = _account.code.length > 0; - if ( - isContract && - rebaseState[_account] == RebaseOptions.NotSet - ) { + if (isContract && rebaseState[_account] == RebaseOptions.NotSet) { _rebaseOptOut(_account); } return alternativeCreditsPerToken[_account] > 0; @@ -515,7 +512,7 @@ contract OUSD is Governable { view returns (uint256) { - // Rounds up, because we need to ensure that accounts allways have + // Rounds up, because we need to ensure that accounts always have // at least the balance that they should have. // Note this should always be used on an absolute account value, // not on a possibly negative diff, because then the rounding would be wrong. @@ -547,7 +544,10 @@ contract OUSD is Governable { } function _rebaseOptIn(address _account) internal { - require(_isNonRebasingAccount(_account), "Account must be non-rebasing"); + require( + _isNonRebasingAccount(_account), + "Account must be non-rebasing" + ); RebaseOptions state = rebaseState[_account]; require( state == RebaseOptions.StdNonRebasing || @@ -692,7 +692,7 @@ contract OUSD is Governable { } function undelegateYield(address from) external onlyGovernor nonReentrant { - // Require a delegation, which will also ensure a vaild delegation + // Require a delegation, which will also ensure a valid delegation require(yieldTo[from] != address(0), ""); address to = yieldTo[from]; From 381d13d38b468cd432e6e2b10e4b08eed4030305 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 7 Nov 2024 19:19:45 +1100 Subject: [PATCH 014/110] Unit test fixes --- contracts/test/token/ousd.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/contracts/test/token/ousd.js b/contracts/test/token/ousd.js index 8b7cde82a0..3dcbcb18ab 100644 --- a/contracts/test/token/ousd.js +++ b/contracts/test/token/ousd.js @@ -442,7 +442,8 @@ describe("Token", function () { const creditsAdded = ousdUnits("99.50") .mul(rebasingCreditsPerTokenHighres) - .div(utils.parseUnits("1", 18)); + .div(utils.parseUnits("1", 18)) + .add(1); await expect(rebasingCredits).to.equal( initialRebasingCredits.add(creditsAdded) @@ -500,7 +501,7 @@ describe("Token", function () { it("Should not allow EOA to call rebaseOptIn when already opted in to rebasing", async () => { let { ousd, matt } = fixture; await expect(ousd.connect(matt).rebaseOptIn()).to.be.revertedWith( - "Account has not opted out" + "Account must be non-rebasing" ); }); @@ -508,7 +509,7 @@ describe("Token", function () { let { ousd, matt } = fixture; await ousd.connect(matt).rebaseOptOut(); await expect(ousd.connect(matt).rebaseOptOut()).to.be.revertedWith( - "Account has not opted in" + "Only standard rebasing accounts can opt out" ); }); @@ -516,14 +517,14 @@ describe("Token", function () { let { mockNonRebasing } = fixture; await mockNonRebasing.rebaseOptIn(); await expect(mockNonRebasing.rebaseOptIn()).to.be.revertedWith( - "Account has not opted out" + "Account must be non-rebasing" ); }); it("Should not allow contract to call rebaseOptOut when already opted out of rebasing", async () => { let { mockNonRebasing } = fixture; await expect(mockNonRebasing.rebaseOptOut()).to.be.revertedWith( - "Account has not opted in" + "Account must be rebasing" ); }); From 4ee1b4bd765ad0c3ba776c230966cf0cfcb99996 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Thu, 7 Nov 2024 10:18:49 +0100 Subject: [PATCH 015/110] fix initialise function sigature and visibility of functions --- contracts/contracts/echidna/EchidnaSetup.sol | 2 +- contracts/contracts/token/OUSD.sol | 30 +++++++++----------- contracts/deploy/deployActions.js | 4 +-- 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/contracts/contracts/echidna/EchidnaSetup.sol b/contracts/contracts/echidna/EchidnaSetup.sol index 210f4ca8e6..03bb826e73 100644 --- a/contracts/contracts/echidna/EchidnaSetup.sol +++ b/contracts/contracts/echidna/EchidnaSetup.sol @@ -19,7 +19,7 @@ contract EchidnaSetup is EchidnaConfig { * @notice Deploy the OUSD contract and set up initial state */ constructor() { - ousd.initialize("Origin Dollar", "OUSD", ADDRESS_VAULT, 1e18); + ousd.initialize(ADDRESS_VAULT, 1e18); // Deploy dummny contracts as users Dummy outsider = new Dummy(); diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 4223f1bac3..052ab68213 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -57,8 +57,6 @@ contract OUSD is Governable { uint256 private constant RESOLUTION_INCREASE = 1e9; function initialize( - string calldata, - string calldata, address _vaultAddress, uint256 _initialCreditsPerToken ) external onlyGovernor { @@ -91,35 +89,35 @@ contract OUSD is Governable { /** * @return The total supply of OUSD. */ - function totalSupply() public view returns (uint256) { + function totalSupply() external view returns (uint256) { return _totalSupply; } /** * @return High resolution rebasingCreditsPerToken */ - function rebasingCreditsPerTokenHighres() public view returns (uint256) { + function rebasingCreditsPerTokenHighres() external view returns (uint256) { return _rebasingCreditsPerToken; } /** * @return Low resolution rebasingCreditsPerToken */ - function rebasingCreditsPerToken() public view returns (uint256) { + function rebasingCreditsPerToken() external view returns (uint256) { return _rebasingCreditsPerToken / RESOLUTION_INCREASE; } /** * @return High resolution total number of rebasing credits */ - function rebasingCreditsHighres() public view returns (uint256) { + function rebasingCreditsHighres() external view returns (uint256) { return _rebasingCredits; } /** * @return Low resolution total number of rebasing credits */ - function rebasingCredits() public view returns (uint256) { + function rebasingCredits() external view returns (uint256) { return _rebasingCredits / RESOLUTION_INCREASE; } @@ -207,7 +205,7 @@ contract OUSD is Governable { * @param _value the amount to be transferred. * @return true on success. */ - function transfer(address _to, uint256 _value) public returns (bool) { + function transfer(address _to, uint256 _value) external returns (bool) { require(_to != address(0), "Transfer to zero address"); _executeTransfer(msg.sender, _to, _value); @@ -227,7 +225,7 @@ contract OUSD is Governable { address _from, address _to, uint256 _value - ) public returns (bool) { + ) external returns (bool) { require(_to != address(0), "Transfer to zero address"); _allowances[_from][msg.sender] = @@ -347,7 +345,7 @@ contract OUSD is Governable { * @return The number of tokens still available for the _spender. */ function allowance(address _owner, address _spender) - public + external view returns (uint256) { @@ -360,7 +358,7 @@ contract OUSD is Governable { * @param _spender The address which will spend the funds. * @param _value The amount of tokens to be spent. */ - function approve(address _spender, uint256 _value) public returns (bool) { + function approve(address _spender, uint256 _value) external returns (bool) { _allowances[msg.sender][_spender] = _value; emit Approval(msg.sender, _spender, _value); return true; @@ -375,7 +373,7 @@ contract OUSD is Governable { * @param _addedValue The amount of tokens to increase the allowance by. */ function increaseAllowance(address _spender, uint256 _addedValue) - public + external returns (bool) { uint256 updatedAllowance = _allowances[msg.sender][_spender] + @@ -393,7 +391,7 @@ contract OUSD is Governable { * by. */ function decreaseAllowance(address _spender, uint256 _subtractedValue) - public + external returns (bool) { uint256 oldValue = _allowances[msg.sender][_spender]; @@ -527,7 +525,7 @@ contract OUSD is Governable { * @param _account Address of the account. */ function governanceRebaseOptIn(address _account) - public + external nonReentrant onlyGovernor { @@ -539,7 +537,7 @@ contract OUSD is Governable { * address's balance will be part of rebases and the account will be exposed * to upside and downside. */ - function rebaseOptIn() public nonReentrant { + function rebaseOptIn() external nonReentrant { _rebaseOptIn(msg.sender); } @@ -569,7 +567,7 @@ contract OUSD is Governable { emit AccountRebasingEnabled(_account); } - function rebaseOptOut() public nonReentrant { + function rebaseOptOut() external nonReentrant { _rebaseOptOut(msg.sender); } diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index 4944a896e5..0e8da016ea 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -1174,7 +1174,7 @@ const deployOETHCore = async () => { await withConfirmation( cOETH .connect(sGovernor) - .initialize("Origin Ether", "OETH", cOETHVaultProxy.address, resolution) + .initialize(cOETHVaultProxy.address, resolution) ); log("Initialized OETH"); }; @@ -1262,7 +1262,7 @@ const deployOUSDCore = async () => { await withConfirmation( cOUSD .connect(sGovernor) - .initialize("Origin Dollar", "OUSD", cVaultProxy.address, resolution) + .initialize(cVaultProxy.address, resolution) ); log("Initialized OUSD"); }; From 9a29c45205045cfdf4a27cb846405b1cb9e76834 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Thu, 7 Nov 2024 12:26:09 +0100 Subject: [PATCH 016/110] fix some tests --- contracts/test/token/ousd.js | 28 ++++++---------------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/contracts/test/token/ousd.js b/contracts/test/token/ousd.js index 3dcbcb18ab..4085a8d83d 100644 --- a/contracts/test/token/ousd.js +++ b/contracts/test/token/ousd.js @@ -852,26 +852,6 @@ describe("Token", function () { }); describe("Delegating yield", function () { - // TODO delete below test later - it("Figure out gas costs", async () => { - let { ousd, matt, josh, anna, governor } = fixture; - - await expect(josh).has.an.approxBalanceOf("100", ousd); - await expect(matt).has.an.approxBalanceOf("100", ousd); - await expect(anna).has.an.approxBalanceOf("0", ousd); - - await ousd - .connect(governor) - // matt delegates yield to anna - .governanceDelegateYield(matt.address, anna.address); - - // await ousd.connect(josh).transfer(matt.address, ousdUnits("2")); - // await ousd.connect(josh).transfer(matt.address, ousdUnits("2")); - - await ousd.connect(matt).transfer(josh.address, ousdUnits("2")); - await ousd.connect(matt).transfer(josh.address, ousdUnits("2")); - }); - it("Should delegate rebase to another account", async () => { let { ousd, vault, matt, josh, anna, usdc, governor } = fixture; @@ -882,10 +862,12 @@ describe("Token", function () { await expect(matt).has.an.approxBalanceOf("80.00", ousd); await expect(anna).has.an.approxBalanceOf("10", ousd); + // TODO: delete rebase opt out later + await ousd.connect(matt).rebaseOptOut(); await ousd .connect(governor) // matt delegates yield to anna - .governanceDelegateYield(matt.address, anna.address); + .delegateYield(matt.address, anna.address); // Transfer USDC into the Vault to simulate yield await usdc.connect(matt).transfer(vault.address, usdcUnits("200")); @@ -919,10 +901,12 @@ describe("Token", function () { await expect(matt).has.an.approxBalanceOf("100.00", ousd); await expect(anna).has.an.balanceOf("0", ousd); + // TODO: delete rebase opt out later + await ousd.connect(matt).rebaseOptOut(); await ousd .connect(governor) // matt delegates yield to anna - .governanceDelegateYield(matt.address, anna.address); + .delegateYield(matt.address, anna.address); // Transfer USDC into the Vault to simulate yield await usdc.connect(matt).transfer(vault.address, usdcUnits("200")); From 47e529f0556577378f11c8f9d0812f2085ddcb88 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Fri, 8 Nov 2024 00:33:56 +0100 Subject: [PATCH 017/110] add explanation for the alternativeCreditsPerToken check --- contracts/contracts/token/OUSD.sol | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 052ab68213..8738ffc0a5 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -499,7 +499,14 @@ contract OUSD is Governable { */ function _isNonRebasingAccount(address _account) internal returns (bool) { bool isContract = _account.code.length > 0; - if (isContract && rebaseState[_account] == RebaseOptions.NotSet) { + // In the older contract implementation: https://github.com/OriginProtocol/origin-dollar/blob/20a21d00a4a6ea9f42940ac194e82655fcda882e/contracts/contracts/token/OUSD.sol#L479-L489 + // a an account could have non 0 balance, be (or become) a contract with the rebase state + // set to default balanceRebaseOptions.NotSet and alternativeCreditsPerToken > 0. The latter would happen + // when such account would already be once `migrated` by running `_ensureRebasingMigration`. Executing the + // migration for a second time would cause great errors. + // With the current code that is no longer possible since accounts have their rebaseState marked + // as `StdNonRebasing` when running `_rebaseOptOut` + if (isContract && rebaseState[_account] == RebaseOptions.NotSet && alternativeCreditsPerToken[_account] == 0) { _rebaseOptOut(_account); } return alternativeCreditsPerToken[_account] > 0; From 24f099bb8370b3c45e40ba13b10de42652b6b8af Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Fri, 8 Nov 2024 00:37:43 +0100 Subject: [PATCH 018/110] prettier --- contracts/contracts/token/OUSD.sol | 16 ++++++++++------ contracts/deploy/deployActions.js | 8 ++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 8738ffc0a5..191c858d16 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -56,10 +56,10 @@ contract OUSD is Governable { uint256 private constant RESOLUTION_INCREASE = 1e9; - function initialize( - address _vaultAddress, - uint256 _initialCreditsPerToken - ) external onlyGovernor { + function initialize(address _vaultAddress, uint256 _initialCreditsPerToken) + external + onlyGovernor + { require(vaultAddress == address(0), "Already initialized"); require(_rebasingCreditsPerToken == 0, "Already initialized"); _rebasingCreditsPerToken = _initialCreditsPerToken; @@ -502,11 +502,15 @@ contract OUSD is Governable { // In the older contract implementation: https://github.com/OriginProtocol/origin-dollar/blob/20a21d00a4a6ea9f42940ac194e82655fcda882e/contracts/contracts/token/OUSD.sol#L479-L489 // a an account could have non 0 balance, be (or become) a contract with the rebase state // set to default balanceRebaseOptions.NotSet and alternativeCreditsPerToken > 0. The latter would happen - // when such account would already be once `migrated` by running `_ensureRebasingMigration`. Executing the + // when such account would already be once `migrated` by running `_ensureRebasingMigration`. Executing the // migration for a second time would cause great errors. // With the current code that is no longer possible since accounts have their rebaseState marked // as `StdNonRebasing` when running `_rebaseOptOut` - if (isContract && rebaseState[_account] == RebaseOptions.NotSet && alternativeCreditsPerToken[_account] == 0) { + if ( + isContract && + rebaseState[_account] == RebaseOptions.NotSet && + alternativeCreditsPerToken[_account] == 0 + ) { _rebaseOptOut(_account); } return alternativeCreditsPerToken[_account] > 0; diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index 0e8da016ea..2c6d7d5a1e 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -1172,9 +1172,7 @@ const deployOETHCore = async () => { */ const resolution = ethers.utils.parseUnits("1", 27); await withConfirmation( - cOETH - .connect(sGovernor) - .initialize(cOETHVaultProxy.address, resolution) + cOETH.connect(sGovernor).initialize(cOETHVaultProxy.address, resolution) ); log("Initialized OETH"); }; @@ -1260,9 +1258,7 @@ const deployOUSDCore = async () => { */ const resolution = ethers.utils.parseUnits("1", 27); await withConfirmation( - cOUSD - .connect(sGovernor) - .initialize(cVaultProxy.address, resolution) + cOUSD.connect(sGovernor).initialize(cVaultProxy.address, resolution) ); log("Initialized OUSD"); }; From 22c1cf5c8536b1b884fdcb4d815a2920581aa5f5 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Fri, 8 Nov 2024 00:39:37 +0100 Subject: [PATCH 019/110] reintroduce the original check --- contracts/contracts/token/OUSD.sol | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 191c858d16..a8002e45e7 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -583,14 +583,11 @@ contract OUSD is Governable { } function _rebaseOptOut(address _account) internal { + require( + alternativeCreditsPerToken[_account] == 0, + "Account must be rebasing" + ); RebaseOptions state = rebaseState[_account]; - if (state == RebaseOptions.StdRebasing) { - require( - alternativeCreditsPerToken[_account] == 0, - "Account must be rebasing" - ); - } - require( state == RebaseOptions.StdRebasing || state == RebaseOptions.NotSet, "Only standard rebasing accounts can opt out" From a5269b87b88d52193d5af7bd29530556bd3e4d82 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Fri, 8 Nov 2024 05:22:16 +0100 Subject: [PATCH 020/110] explicitly call rebaseOpt out if the yield delegating account hasn't opted out already --- contracts/contracts/token/OUSD.sol | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index a8002e45e7..e345da5bc2 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -500,7 +500,7 @@ contract OUSD is Governable { function _isNonRebasingAccount(address _account) internal returns (bool) { bool isContract = _account.code.length > 0; // In the older contract implementation: https://github.com/OriginProtocol/origin-dollar/blob/20a21d00a4a6ea9f42940ac194e82655fcda882e/contracts/contracts/token/OUSD.sol#L479-L489 - // a an account could have non 0 balance, be (or become) a contract with the rebase state + // an account could have non 0 balance, be (or become) a contract with the rebase state // set to default balanceRebaseOptions.NotSet and alternativeCreditsPerToken > 0. The latter would happen // when such account would already be once `migrated` by running `_ensureRebasingMigration`. Executing the // migration for a second time would cause great errors. @@ -665,12 +665,11 @@ contract OUSD is Governable { ); RebaseOptions stateFrom = rebaseState[from]; RebaseOptions stateTo = rebaseState[to]; - require( - _isNonRebasingAccount(from) && + if (!_isNonRebasingAccount(from) && (stateFrom == RebaseOptions.NotSet || - stateFrom == RebaseOptions.StdNonRebasing), - "Must delegate from a non-rebasing account" - ); + stateFrom == RebaseOptions.StdRebasing)) { + _rebaseOptOut(from); + } require( !_isNonRebasingAccount(to) && (stateTo == RebaseOptions.NotSet || From e64ab178b381d187e69d129e6492a384e506598a Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Fri, 8 Nov 2024 07:47:20 +0100 Subject: [PATCH 021/110] add one more test --- contracts/test/token/ousd.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/contracts/test/token/ousd.js b/contracts/test/token/ousd.js index 4085a8d83d..198605fedb 100644 --- a/contracts/test/token/ousd.js +++ b/contracts/test/token/ousd.js @@ -509,7 +509,7 @@ describe("Token", function () { let { ousd, matt } = fixture; await ousd.connect(matt).rebaseOptOut(); await expect(ousd.connect(matt).rebaseOptOut()).to.be.revertedWith( - "Only standard rebasing accounts can opt out" + "Account must be rebasing" ); }); @@ -522,12 +522,21 @@ describe("Token", function () { }); it("Should not allow contract to call rebaseOptOut when already opted out of rebasing", async () => { - let { mockNonRebasing } = fixture; + let { mockNonRebasing, ousd, matt } = fixture; + // send some OUSD to trigger the automatic "migration" of mockNonRebasing account to nonRebasing + await ousd.connect(matt).transfer(mockNonRebasing.address, ousdUnits("1")); + await expect(mockNonRebasing.rebaseOptOut()).to.be.revertedWith( "Account must be rebasing" ); }); + it("Should allow a contract to call rebaseOptOut if no other action causing auto-converting has happened", async () => { + let { mockNonRebasing } = fixture; + + await mockNonRebasing.rebaseOptOut(); + }); + it("Should maintain the correct balance on a partial transfer for a non-rebasing account without previously set creditsPerToken", async () => { let { ousd, matt, josh, mockNonRebasing } = fixture; From 68d14bd4cc7401a6f6ed5f23a724f4455880d28e Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Fri, 8 Nov 2024 09:20:10 +0100 Subject: [PATCH 022/110] add Readme of the token contract logic --- .../contracts/token/README-token-logic.md | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 contracts/contracts/token/README-token-logic.md diff --git a/contracts/contracts/token/README-token-logic.md b/contracts/contracts/token/README-token-logic.md new file mode 100644 index 0000000000..3670367181 --- /dev/null +++ b/contracts/contracts/token/README-token-logic.md @@ -0,0 +1,97 @@ +# OUSD Token: Version 4.0 + +We are revamping the our rebasing token contract. + +The primary objective is to allow delegated yield. Delegated yield allows an account to seemlessly transfer all earned yield to another account. + +Secondarily, we'd like to fix the tiny rounding issues around transfers and between local account information and global tracking slots. + + +## How OUSD works. + +OUSD is a rebasing token. It's mission in life is to be able to distribute increases in backing assets on to users by having user's balances go up each time the token rebases. + +**`_rebasingCreditsPerToken`** is a global variable that converts between "credits" stored on a account, and the actual balance of the account. This allows this single variable to be updated and in turn all "rebasing" users have their account balance change proportionally. Counterintuitively, this is not a multiplier on users credits, but a divider. So it's `user balance = user credits / _rebasingCreditsPerToken`. Because it's a divider, OUSD will slowly lose resolution over very long timeframes, as opposed to abruptly stopping working suddenly once enough yield has been earned. + +**_creditBalances[account]** This per account mapping stores the internal credits for each account. + +**alternativeCreditsPerToken[account]** This per account mapping stores an alternative, optional conversion factor for the value used in creditBalances. When it is set to zero, it means that it is unused, and the global `_rebasingCreditsPerToken` should be used instead. Because this alternative conversion factor does not update on rebases, it allows an account to be "frozen" and no longer change balances as rebases happen. + +**rebaseState[account]** This holds user preferences for what type of accounting is used on an account. For historical reasons the default, `NotSet` value on this could mean that the account is using either `StdRebasing` or `StdNonRebasing` accounting (see details later). + +### StdRebasing Account (Default) + +This is the "normal" account in the system. It gets yield and its balance goes up over time. Almost every account is of this type. + +Reads: + +- `rebaseState`: could be either `NotSet` or `StdRebasing`. Almost all accounts are `NotSet`, and typically only contracts that want to receive yield are set to `StdRebasing` (though there's nothing preventing regular users from explicitly marking their account as receiving yield). +- `alternativeCreditsPerToken`: will always be zero, thus using the global _rebasingCreditsPerToken +- `_creditBalances`: credits for the account + +Writes: + +- `rebaseState`: if explicitly moving to this state from another state `StdRebasing` is set. Otherwise, the account remains `NotSet`. +- `alternativeCreditsPerToken`: will always be zero +- `_creditBalances`: credits for the account + +Transitions to: + +- automatic conversion to a `StdNonRebasing` account if funds are moved to or from a contract AND the account is currently `NotSet`. +- to `StdNonRebasing` if the account calls `rebaseOptOut()` +- to `YieldDelegationSource` if the source account in a `delegateYield()` call +- to `YieldDelegationTarget` if it is the destination account in a `delegateYield()` + +### StdNonRebasing Account (Default) + +This account does not earn yield. It was originally created for backwards compatibility with systems that did not support balance changes, as well not wasting yield on third party contracts holding tokens that did not support any distribution to users. As a side benefit, regular users earn at a higher rate than the increase in assets. + +Reads: + +- `rebaseState`: could be either `NotSet` or `StdNonRebasing`. Historically, almost all accounts are `NotSet` and you can only determine which kind of account `NotSet` is by looking at `alternativeCreditsPerToken`. +- `alternativeCreditsPerToken` Will always be non-zero. Probably ranges from 1e17-ish to 1e27, with most at 1e27. +- `_creditBalances` will either be a "frozen credits style" that can be converted via `alternativeCreditsPerToken`, or "frozen balance" style, losslessly convertible via an 1e18 or 1e27 in `alternativeCreditsPerToken`. + +Writes: + +- `rebaseState`: Set to `StdNonRebasing` when new contracts are automatically moved to this state, or when explicitly converted to this account type. This was not previously the case for historical automatic conversions. +- `alternativeCreditsPerToken`: New balance writes will always use 1e18, which will result in the account's credits being equal to the balance. +- `_creditBalances`: New balance writes will always use 1:1 a credits/balance ratio, which will make this be the account balance. + +Transitions to: + +- to `StdRebasing` via a `rebaseOptIn()` call or a governance `governanceRebaseOptIn()`. +- to `YieldDelegationSource` if the source account in a `delegateYield()` call +- to `YieldDelegationTarget` if it is the destination account in a `delegateYield()` + +### YieldDelegationSource + +This account does not earn yield, instead its yield is passed on to another account. + +It does this by keeping a non-rebasing style fixed balance locally, while storing all its rebasing credits on the target account. This makes the target account's credits be `(target account's credits + source account's credits)` + +Reads / Writes (no historical accounts to deal with!): + +- `rebaseState`: `YieldDelegationSource` +- `alternativeCreditsPerToken`: Always 1e18. +- `_creditBalances`: Always set to the account balance in 1:1 credits. +- Target account's `_creditBalances`: Increased by this accounts credits at the global `_rebasingCreditsPerToken`. + +Transitions to: +- to `StdNonRebasing` if `undelegateYield()` is called on the yield delegation + +### YieldDelegationTarget + +This account earns extra yield from exactly one account. YieldDelegationTargets can have their own balances, and these balances to do earn. This works by having both account's credits stored in this account, but then subtracting the other account's fixed balance from the total. + +For example, someone loans you an intrest free $10,000. You now have an extra $10,000, but also owe them $10,000 so that nets out to a zero change in your wealth. You take that $10,000 and invest it in T-bills, so you are now getting more yield than you did before. + + +Reads / Writes (no historical accounts to deal with!): +- `rebaseState`: `YieldDelegationTarget` +- `alternativeCreditsPerToken`: Always 0 +- `_creditBalances`: The sum of this account's credits and the yield sources credits. +- Source account's `_creditBalances`: This balance is subtracted by that value + +Transitions to: +- to `StdRebasing` if `undelegateYield()` is called on the yield delegation \ No newline at end of file From ef7c4ab26b56f38ff669de8c151eb4f4a0ae2285 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Fri, 8 Nov 2024 09:48:03 +0100 Subject: [PATCH 023/110] force account to be rebasing on yield delegation --- contracts/contracts/token/OUSD.sol | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index e345da5bc2..e7f340db48 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -670,12 +670,11 @@ contract OUSD is Governable { stateFrom == RebaseOptions.StdRebasing)) { _rebaseOptOut(from); } - require( - !_isNonRebasingAccount(to) && + if(_isNonRebasingAccount(to) && (stateTo == RebaseOptions.NotSet || - stateTo == RebaseOptions.StdRebasing), - "Must delegate to a rebasing account" - ); + stateTo == RebaseOptions.StdNonRebasing)) { + _rebaseOptIn(to); + } // Set up the bidirectional links yieldTo[from] = to; From 90ec917b0c69833de0144685856114b3759b38b6 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 11 Nov 2024 23:25:07 +0100 Subject: [PATCH 024/110] add explicit whitelist of allowed states when delegating yield --- contracts/contracts/token/OUSD.sol | 31 ++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index e7f340db48..78bd8ca0aa 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -665,14 +665,33 @@ contract OUSD is Governable { ); RebaseOptions stateFrom = rebaseState[from]; RebaseOptions stateTo = rebaseState[to]; - if (!_isNonRebasingAccount(from) && - (stateFrom == RebaseOptions.NotSet || - stateFrom == RebaseOptions.StdRebasing)) { + + require( + stateFrom == RebaseOptions.NotSet || + stateFrom == RebaseOptions.StdNonRebasing || + stateFrom == RebaseOptions.StdRebasing, + "Invalid rebaseState from" + ); + + require( + stateTo == RebaseOptions.NotSet || + stateTo == RebaseOptions.StdNonRebasing || + stateTo == RebaseOptions.StdRebasing, + "Invalid rebaseState to" + ); + + if ( + !_isNonRebasingAccount(from) && + (stateFrom == RebaseOptions.NotSet || + stateFrom == RebaseOptions.StdRebasing) + ) { _rebaseOptOut(from); } - if(_isNonRebasingAccount(to) && - (stateTo == RebaseOptions.NotSet || - stateTo == RebaseOptions.StdNonRebasing)) { + if ( + _isNonRebasingAccount(to) && + (stateTo == RebaseOptions.NotSet || + stateTo == RebaseOptions.StdNonRebasing) + ) { _rebaseOptIn(to); } From 2c96306d329b0587cc0a3b77873c09fe74a762ee Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 13 Nov 2024 14:29:48 +0100 Subject: [PATCH 025/110] add more defensive checks where isNonRebasingAccount can not mistankingly be used to return true for yield delegation sources --- contracts/contracts/token/OUSD.sol | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 78bd8ca0aa..d87e01391c 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -513,7 +513,10 @@ contract OUSD is Governable { ) { _rebaseOptOut(_account); } - return alternativeCreditsPerToken[_account] > 0; + + return rebaseState[_account] != RebaseOptions.YieldDelegationSource && + rebaseState[_account] != RebaseOptions.YieldDelegationTarget && + alternativeCreditsPerToken[_account] > 0; } function _balanceToRebasingCredits(uint256 balance) From 4bf3360d204d317a7c662a40df20f148ce73db53 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 13 Nov 2024 14:45:39 +0100 Subject: [PATCH 026/110] remove increase/decrease allowance --- contracts/contracts/echidna/EchidnaHelper.sol | 36 --------------- .../contracts/echidna/EchidnaTestApproval.sol | 42 ------------------ contracts/contracts/mocks/MockNonRebasing.sol | 4 +- contracts/contracts/token/OUSD.sol | 42 ------------------ contracts/test/token/ousd.js | 44 +++---------------- 5 files changed, 7 insertions(+), 161 deletions(-) diff --git a/contracts/contracts/echidna/EchidnaHelper.sol b/contracts/contracts/echidna/EchidnaHelper.sol index 7c3dabef10..b39aa5ed08 100644 --- a/contracts/contracts/echidna/EchidnaHelper.sol +++ b/contracts/contracts/echidna/EchidnaHelper.sol @@ -131,42 +131,6 @@ contract EchidnaHelper is EchidnaSetup { ousd.approve(spender, amount); } - /** - * @notice Increase the allowance of an account to spend OUSD - * @param ownerAcc Account that owns the OUSD - * @param spenderAcc Account that is approved to spend the OUSD - * @param amount Amount to increase the allowance by - */ - function increaseAllowance( - uint8 ownerAcc, - uint8 spenderAcc, - uint256 amount - ) public { - address owner = getAccount(ownerAcc); - address spender = getAccount(spenderAcc); - hevm.prank(owner); - // slither-disable-next-line unused-return - ousd.increaseAllowance(spender, amount); - } - - /** - * @notice Decrease the allowance of an account to spend OUSD - * @param ownerAcc Account that owns the OUSD - * @param spenderAcc Account that is approved to spend the OUSD - * @param amount Amount to decrease the allowance by - */ - function decreaseAllowance( - uint8 ownerAcc, - uint8 spenderAcc, - uint256 amount - ) public { - address owner = getAccount(ownerAcc); - address spender = getAccount(spenderAcc); - hevm.prank(owner); - // slither-disable-next-line unused-return - ousd.decreaseAllowance(spender, amount); - } - /** * @notice Get the sum of all OUSD balances * @return total Total balance diff --git a/contracts/contracts/echidna/EchidnaTestApproval.sol b/contracts/contracts/echidna/EchidnaTestApproval.sol index c95543fdb3..a4f7d9b0b2 100644 --- a/contracts/contracts/echidna/EchidnaTestApproval.sol +++ b/contracts/contracts/echidna/EchidnaTestApproval.sol @@ -94,46 +94,4 @@ contract EchidnaTestApproval is EchidnaTestMintBurn { assert(allowanceAfter2 == amount / 2); } - - /** - * @notice Increasing the allowance should raise it by the amount provided - * @param ownerAcc The account that is approving - * @param spenderAcc The account that is being approved - * @param amount The amount to approve - */ - function testIncreaseAllowance( - uint8 ownerAcc, - uint8 spenderAcc, - uint256 amount - ) public { - address owner = getAccount(ownerAcc); - address spender = getAccount(spenderAcc); - - uint256 allowanceBefore = ousd.allowance(owner, spender); - increaseAllowance(ownerAcc, spenderAcc, amount); - uint256 allowanceAfter = ousd.allowance(owner, spender); - - assert(allowanceAfter == allowanceBefore + amount); - } - - /** - * @notice Decreasing the allowance should lower it by the amount provided - * @param ownerAcc The account that is approving - * @param spenderAcc The account that is being approved - * @param amount The amount to approve - */ - function testDecreaseAllowance( - uint8 ownerAcc, - uint8 spenderAcc, - uint256 amount - ) public { - address owner = getAccount(ownerAcc); - address spender = getAccount(spenderAcc); - - uint256 allowanceBefore = ousd.allowance(owner, spender); - decreaseAllowance(ownerAcc, spenderAcc, amount); - uint256 allowanceAfter = ousd.allowance(owner, spender); - - assert(allowanceAfter == allowanceBefore - amount); - } } diff --git a/contracts/contracts/mocks/MockNonRebasing.sol b/contracts/contracts/mocks/MockNonRebasing.sol index b94fc96261..ab835d990a 100644 --- a/contracts/contracts/mocks/MockNonRebasing.sol +++ b/contracts/contracts/mocks/MockNonRebasing.sol @@ -34,8 +34,8 @@ contract MockNonRebasing { oUSD.transferFrom(_from, _to, _value); } - function increaseAllowance(address _spender, uint256 _addedValue) public { - oUSD.increaseAllowance(_spender, _addedValue); + function approve(address _spender, uint256 _addedValue) public { + oUSD.approve(_spender, _addedValue); } function mintOusd( diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index d87e01391c..19bb32badb 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -364,48 +364,6 @@ contract OUSD is Governable { return true; } - /** - * @dev Increase the amount of tokens that an owner has allowed to - * `_spender`. - * This method should be used instead of approve() to avoid the double - * approval vulnerability described above. - * @param _spender The address which will spend the funds. - * @param _addedValue The amount of tokens to increase the allowance by. - */ - function increaseAllowance(address _spender, uint256 _addedValue) - external - returns (bool) - { - uint256 updatedAllowance = _allowances[msg.sender][_spender] + - _addedValue; - _allowances[msg.sender][_spender] = updatedAllowance; - emit Approval(msg.sender, _spender, updatedAllowance); - return true; - } - - /** - * @dev Decrease the amount of tokens that an owner has allowed to - `_spender`. - * @param _spender The address which will spend the funds. - * @param _subtractedValue The amount of tokens to decrease the allowance - * by. - */ - function decreaseAllowance(address _spender, uint256 _subtractedValue) - external - returns (bool) - { - uint256 oldValue = _allowances[msg.sender][_spender]; - uint256 newValue; - if (_subtractedValue >= oldValue) { - newValue = 0; - } else { - newValue = oldValue - _subtractedValue; - } - _allowances[msg.sender][_spender] = newValue; - emit Approval(msg.sender, _spender, newValue); - return true; - } - /** * @dev Mints new tokens, increasing totalSupply. */ diff --git a/contracts/test/token/ousd.js b/contracts/test/token/ousd.js index 198605fedb..945fd3d329 100644 --- a/contracts/test/token/ousd.js +++ b/contracts/test/token/ousd.js @@ -250,7 +250,8 @@ describe("Token", function () { // Give Josh an allowance to move Matt's OUSD await ousd .connect(matt) - .increaseAllowance(await josh.getAddress(), ousdUnits("100")); + .approve(await josh.getAddress(), ousdUnits("100")); + // Give contract 100 OUSD from Matt via Josh await ousd .connect(josh) @@ -288,7 +289,7 @@ describe("Token", function () { // Give Josh an allowance to move Matt's OUSD await ousd .connect(matt) - .increaseAllowance(await josh.getAddress(), ousdUnits("150")); + .approve(await josh.getAddress(), ousdUnits("150")); // Give contract 100 OUSD from Matt via Josh await ousd .connect(josh) @@ -333,7 +334,7 @@ describe("Token", function () { await expect(matt).has.an.approxBalanceOf("100.00", ousd); await expect(josh).has.an.approxBalanceOf("0", ousd); await expect(mockNonRebasing).has.an.approxBalanceOf("100.00", ousd); - await mockNonRebasing.increaseAllowance( + await mockNonRebasing.approve( await matt.getAddress(), ousdUnits("100") ); @@ -380,7 +381,7 @@ describe("Token", function () { await expect(matt).has.an.approxBalanceOf("250", ousd); await expect(mockNonRebasing).has.an.approxBalanceOf("150.00", ousd); // Transfer contract balance to Josh - await mockNonRebasing.increaseAllowance( + await mockNonRebasing.approve( await matt.getAddress(), ousdUnits("150") ); @@ -628,39 +629,6 @@ describe("Token", function () { ).to.be.revertedWith("panic code 0x11"); }); - it("Should allow to increase/decrease allowance", async () => { - const { ousd, anna, matt } = fixture; - // Approve OUSD - await ousd.connect(matt).approve(anna.getAddress(), ousdUnits("1000")); - expect( - await ousd.allowance(await matt.getAddress(), await anna.getAddress()) - ).to.equal(ousdUnits("1000")); - - // Decrease allowance - await ousd - .connect(matt) - .decreaseAllowance(await anna.getAddress(), ousdUnits("100")); - expect( - await ousd.allowance(await matt.getAddress(), await anna.getAddress()) - ).to.equal(ousdUnits("900")); - - // Increase allowance - await ousd - .connect(matt) - .increaseAllowance(await anna.getAddress(), ousdUnits("20")); - expect( - await ousd.allowance(await matt.getAddress(), await anna.getAddress()) - ).to.equal(ousdUnits("920")); - - // Decrease allowance more than what's there - await ousd - .connect(matt) - .decreaseAllowance(await anna.getAddress(), ousdUnits("950")); - expect( - await ousd.allowance(await matt.getAddress(), await anna.getAddress()) - ).to.equal(ousdUnits("0")); - }); - it("Should increase users balance on supply increase", async () => { const { ousd, usdc, vault, anna, matt } = fixture; // Transfer 1 to Anna, so we can check different amounts @@ -893,9 +861,7 @@ describe("Token", function () { await expect(matt).has.an.approxBalanceOf("80.00", ousd); await expect(anna).has.an.balanceOf("90", ousd); - console.log("Matt transfering to josh"); await ousd.connect(matt).transfer(josh.address, ousdUnits("80")); - console.log("Anna transfering to josh"); await ousd.connect(anna).transfer(josh.address, ousdUnits("90")); await expect(josh).has.an.approxBalanceOf("400", ousd); From 5cbcf4c9dc46a2a0140870fabf2aeb721bd402f6 Mon Sep 17 00:00:00 2001 From: Daniel Von Fange Date: Wed, 13 Nov 2024 08:51:36 -0500 Subject: [PATCH 027/110] Update docs with invarients --- .../contracts/token/README-token-logic.md | 80 ++++++++++++++++++- 1 file changed, 77 insertions(+), 3 deletions(-) diff --git a/contracts/contracts/token/README-token-logic.md b/contracts/contracts/token/README-token-logic.md index 3670367181..8192405dfd 100644 --- a/contracts/contracts/token/README-token-logic.md +++ b/contracts/contracts/token/README-token-logic.md @@ -2,7 +2,7 @@ We are revamping the our rebasing token contract. -The primary objective is to allow delegated yield. Delegated yield allows an account to seemlessly transfer all earned yield to another account. +The primary objective is to allow delegated yield. Delegated yield allows an account to seamlessly transfer all earned yield to another account. Secondarily, we'd like to fix the tiny rounding issues around transfers and between local account information and global tracking slots. @@ -13,12 +13,16 @@ OUSD is a rebasing token. It's mission in life is to be able to distribute incre **`_rebasingCreditsPerToken`** is a global variable that converts between "credits" stored on a account, and the actual balance of the account. This allows this single variable to be updated and in turn all "rebasing" users have their account balance change proportionally. Counterintuitively, this is not a multiplier on users credits, but a divider. So it's `user balance = user credits / _rebasingCreditsPerToken`. Because it's a divider, OUSD will slowly lose resolution over very long timeframes, as opposed to abruptly stopping working suddenly once enough yield has been earned. -**_creditBalances[account]** This per account mapping stores the internal credits for each account. +**_creditBalances[account]** This per account mapping stores the internal credits for each account. **alternativeCreditsPerToken[account]** This per account mapping stores an alternative, optional conversion factor for the value used in creditBalances. When it is set to zero, it means that it is unused, and the global `_rebasingCreditsPerToken` should be used instead. Because this alternative conversion factor does not update on rebases, it allows an account to be "frozen" and no longer change balances as rebases happen. **rebaseState[account]** This holds user preferences for what type of accounting is used on an account. For historical reasons the default, `NotSet` value on this could mean that the account is using either `StdRebasing` or `StdNonRebasing` accounting (see details later). +**totalSupply** Notationally the sum of all account balances. + +## Account Types + ### StdRebasing Account (Default) This is the "normal" account in the system. It gets yield and its balance goes up over time. Almost every account is of this type. @@ -94,4 +98,74 @@ Reads / Writes (no historical accounts to deal with!): - Source account's `_creditBalances`: This balance is subtracted by that value Transitions to: -- to `StdRebasing` if `undelegateYield()` is called on the yield delegation \ No newline at end of file +- to `StdRebasing` if `undelegateYield()` is called on the yield delegation + +## Account invariants + + + +> Any account with a zero value in `alternativeCreditsPerToken` has a `rebaseState` that is one of (NotSet, StdRebasing, or YieldDelegationTarget) [^1] + + +> Any account with value of 1e18 in `alternativeCreditsPerToken` has a `rebaseState` that is one of (StdNonRebasing, YieldDelegationSource) [^1] + + +> `alternativeCreditsPerToken` can only be set to 0 or 1e18, no other values [^1] + + +> Any account with `rebaseState` = `YieldDelegationSource` has a nonZero `yieldTo` + + +> Any account with `rebaseState` = `YieldDelegationTarget` has a nonZero `yieldFrom` + + +> Any non zero `YieldFrom` points to an account that has a `YieldTo` pointing back to it + + +## Balance Invariants + +There are four different account types, two of which link to each other behind the scenes. Because of this, checks on overall balances cannot only look at the to / from accounts in a transfer. + + +> No non-vault accounts cannot increase or decrease the sum of all balances. (This covers all actions including optIn/out, and yield delegation, not just transfers) [^2] + + +> The from account in a transfer should have its balance reduced by the amount of the transfer, [^2] + + +> The To account in a transfer should have its balance increased by the amount of the transfer. [^2] + + +> The sum of all account balanceOf's is less or equal to than the totalSupply [^2] + + +> The sum of all `RebaseOptions.StdNonRebasing` accounts equals the nonRebasingSupply. [^1] [^2] + + +> The sum of the credits in all NotSet, StdRebasing, and YieldDelegationTarget accounts equal the rebasingCredits. + + +> The balanceOf on each account equals `_creditBalances[account] * (alternativeCreditsPerToken[account] > 0 ? alternativeCreditsPerToken[account] : _rebasingCreditsPerToken) - (yieldFrom[account] == 0 ? 0 : _creditBalances[yieldFrom[account]])` + + +## Rebasing + +The token is designed to gently degrade once a huge amount of APY has been earned. Once this crosses a certain point, and enough resolution is no longer possible, transfers should slightly round up. + +There is inevitable rounding error when rebasing, since there is no possible way to ensure that totalSupply is exactly matched. Total supply moves up exactly as it is set. + + +## Rebasing invariants + + +> After a call to changeSupply() then `nonRebasingCredits + (rebasingCredits / rebasingCreditsPer) <= totalSupply` + + +> After a call to changeSupply(), the new totalSupply should always match what was passed into the call or the call revert. + + + + +[^1]: From the current code base. Historically there may be different data stored in storage slots. + +[^2]: As long as the token has sufficient resolution \ No newline at end of file From 0ab7bc894386fb5eaf85a24bdfc64fbe45edf978 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 13 Nov 2024 15:34:46 +0100 Subject: [PATCH 028/110] Use safecast for any downcasting (#2306) * use safecasts for any downcasting * use safecast when downcasting from int256. Also fix tests * prettier * clean up implementation --- contracts/contracts/token/OUSD.sol | 47 +++++++++++++++--------------- contracts/test/flipper/flipper.js | 4 ++- contracts/test/vault/oeth-vault.js | 2 +- contracts/test/vault/redeem.js | 2 +- 4 files changed, 29 insertions(+), 26 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 19bb32badb..c7632deca2 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -8,6 +8,7 @@ pragma solidity ^0.8.0; * @author Origin Protocol Inc */ import { Governable } from "../governance/Governable.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; /** * NOTE that this is an ERC20 token but the invariant that the sum of @@ -16,6 +17,9 @@ import { Governable } from "../governance/Governable.sol"; */ contract OUSD is Governable { + using SafeCast for int256; + using SafeCast for uint256; + event TotalSupplyUpdatedHighres( uint256 totalSupply, uint256 rebasingCredits, @@ -257,11 +261,11 @@ contract OUSD is Governable { ( int256 fromRebasingCreditsDiff, int256 fromNonRebasingSupplyDiff - ) = _adjustAccount(_from, -int256(_value)); + ) = _adjustAccount(_from, -_value.toInt256()); ( int256 toRebasingCreditsDiff, int256 toNonRebasingSupplyDiff - ) = _adjustAccount(_to, int256(_value)); + ) = _adjustAccount(_to, _value.toInt256()); _adjustGlobals( fromRebasingCreditsDiff + toRebasingCreditsDiff, @@ -274,13 +278,12 @@ contract OUSD is Governable { returns (int256 rebasingCreditsDiff, int256 nonRebasingSupplyDiff) { RebaseOptions state = rebaseState[account]; - int256 currentBalance = int256(balanceOf(account)); - uint256 newBalance = uint256( - int256(currentBalance) + int256(balanceChange) - ); - if (newBalance < 0) { + int256 currentBalance = balanceOf(account).toInt256(); + if (currentBalance + balanceChange < 0) { revert("Transfer amount exceeds balance"); } + uint256 newBalance = (currentBalance + balanceChange).toUint256(); + if (state == RebaseOptions.YieldDelegationSource) { address target = yieldTo[account]; uint256 targetOldBalance = balanceOf(target); @@ -288,8 +291,8 @@ contract OUSD is Governable { targetOldBalance + newBalance ); rebasingCreditsDiff = - int256(targetNewCredits) - - int256(_creditBalances[target]); + targetNewCredits.toInt256() - + _creditBalances[target].toInt256(); _creditBalances[account] = newBalance; _creditBalances[target] = targetNewCredits; @@ -299,8 +302,8 @@ contract OUSD is Governable { newBalance + _creditBalances[yieldFrom[account]] ); rebasingCreditsDiff = - int256(newCredits) - - int256(_creditBalances[account]); + newCredits.toInt256() - + _creditBalances[account].toInt256(); _creditBalances[account] = newCredits; } else if (_isNonRebasingAccount(account)) { nonRebasingSupplyDiff = balanceChange; @@ -309,8 +312,8 @@ contract OUSD is Governable { } else { uint256 newCredits = _balanceToRebasingCredits(newBalance); rebasingCreditsDiff = - int256(newCredits) - - int256(_creditBalances[account]); + newCredits.toInt256() - + _creditBalances[account].toInt256(); _creditBalances[account] = newCredits; } } @@ -320,20 +323,18 @@ contract OUSD is Governable { int256 nonRebasingSupplyDiff ) internal { if (rebasingCreditsDiff != 0) { - if (uint256(int256(_rebasingCredits) + rebasingCreditsDiff) < 0) { + if (_rebasingCredits.toInt256() + rebasingCreditsDiff < 0) { revert("rebasingCredits underflow"); } - _rebasingCredits = uint256( - int256(_rebasingCredits) + rebasingCreditsDiff - ); + _rebasingCredits = (_rebasingCredits.toInt256() + + rebasingCreditsDiff).toUint256(); } if (nonRebasingSupplyDiff != 0) { - if (int256(nonRebasingSupply) + nonRebasingSupplyDiff < 0) { + if (nonRebasingSupply.toInt256() + nonRebasingSupplyDiff < 0) { revert("nonRebasingSupply underflow"); } - nonRebasingSupply = uint256( - int256(nonRebasingSupply) + nonRebasingSupplyDiff - ); + nonRebasingSupply = (nonRebasingSupply.toInt256() + + nonRebasingSupplyDiff).toUint256(); } } @@ -388,7 +389,7 @@ contract OUSD is Governable { ( int256 toRebasingCreditsDiff, int256 toNonRebasingSupplyDiff - ) = _adjustAccount(_account, int256(_amount)); + ) = _adjustAccount(_account, _amount.toInt256()); // Globals _adjustGlobals(toRebasingCreditsDiff, toNonRebasingSupplyDiff); _totalSupply = _totalSupply + _amount; @@ -425,7 +426,7 @@ contract OUSD is Governable { ( int256 toRebasingCreditsDiff, int256 toNonRebasingSupplyDiff - ) = _adjustAccount(_account, -int256(_amount)); + ) = _adjustAccount(_account, -_amount.toInt256()); // Globals _adjustGlobals(toRebasingCreditsDiff, toNonRebasingSupplyDiff); _totalSupply = _totalSupply - _amount; diff --git a/contracts/test/flipper/flipper.js b/contracts/test/flipper/flipper.js index 660a47e8fb..2ef0791aed 100644 --- a/contracts/test/flipper/flipper.js +++ b/contracts/test/flipper/flipper.js @@ -62,7 +62,9 @@ describe("Flipper", function () { // eslint-disable-next-line `buyOusdWith${titleName}` ](ousdUnits("1")); - await expect(call).to.be.revertedWith("Transfer greater than balance"); + await expect(call).to.be.revertedWith( + "Transfer amount exceeds balance" + ); } ); }); diff --git a/contracts/test/vault/oeth-vault.js b/contracts/test/vault/oeth-vault.js index 60d45f1f99..35a3eb9913 100644 --- a/contracts/test/vault/oeth-vault.js +++ b/contracts/test/vault/oeth-vault.js @@ -1428,7 +1428,7 @@ describe("OETH Vault", function () { .connect(josh) .requestWithdrawal(dataBefore.userOeth.add(1)); - await expect(tx).to.revertedWith("Remove exceeds balance"); + await expect(tx).to.revertedWith("Transfer amount exceeds balance"); }); it("capital is paused", async () => { const { oethVault, governor, josh } = fixture; diff --git a/contracts/test/vault/redeem.js b/contracts/test/vault/redeem.js index 78e44366bf..e566c36657 100644 --- a/contracts/test/vault/redeem.js +++ b/contracts/test/vault/redeem.js @@ -154,7 +154,7 @@ describe("Vault Redeem", function () { // Try to withdraw more than balance await expect( vault.connect(anna).redeem(ousdUnits("100.0"), 0) - ).to.be.revertedWith("Remove exceeds balance"); + ).to.be.revertedWith("Transfer amount exceeds balance"); }); it("Should only allow Governor to set a redeem fee", async () => { From 202965990f487785d30b7b19b833c1acf77502a5 Mon Sep 17 00:00:00 2001 From: Daniel Von Fange Date: Wed, 13 Nov 2024 13:33:40 -0500 Subject: [PATCH 029/110] More invarients around outer functions --- .../contracts/token/README-token-logic.md | 34 +++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/contracts/contracts/token/README-token-logic.md b/contracts/contracts/token/README-token-logic.md index 8192405dfd..76ec474274 100644 --- a/contracts/contracts/token/README-token-logic.md +++ b/contracts/contracts/token/README-token-logic.md @@ -4,12 +4,12 @@ We are revamping the our rebasing token contract. The primary objective is to allow delegated yield. Delegated yield allows an account to seamlessly transfer all earned yield to another account. -Secondarily, we'd like to fix the tiny rounding issues around transfers and between local account information and global tracking slots. +Secondarily, we'd like to fix the tiny rounding issues around both transfers and local account information vs global tracking variables. ## How OUSD works. -OUSD is a rebasing token. It's mission in life is to be able to distribute increases in backing assets on to users by having user's balances go up each time the token rebases. +OUSD is a rebasing token. Its mission in life is to be able to distribute increases in backing assets on to users by having user's balances go up each time the token rebases. **`_rebasingCreditsPerToken`** is a global variable that converts between "credits" stored on a account, and the actual balance of the account. This allows this single variable to be updated and in turn all "rebasing" users have their account balance change proportionally. Counterintuitively, this is not a multiplier on users credits, but a divider. So it's `user balance = user credits / _rebasingCreditsPerToken`. Because it's a divider, OUSD will slowly lose resolution over very long timeframes, as opposed to abruptly stopping working suddenly once enough yield has been earned. @@ -23,9 +23,13 @@ OUSD is a rebasing token. It's mission in life is to be able to distribute incre ## Account Types +There are four account types in the system. + +The new code is more explicit in its writes than the old code. Thus there's two sections for each type, the old values that could be read, and the format of the new values that the new code writes. + ### StdRebasing Account (Default) -This is the "normal" account in the system. It gets yield and its balance goes up over time. Almost every account is of this type. +This is the "normal" account in the system. It receives yield and its balance goes up over time. Almost every account is of this type. Reads: @@ -142,7 +146,7 @@ There are four different account types, two of which link to each other behind t > The sum of all `RebaseOptions.StdNonRebasing` accounts equals the nonRebasingSupply. [^1] [^2] -> The sum of the credits in all NotSet, StdRebasing, and YieldDelegationTarget accounts equal the rebasingCredits. +> The sum of the credits in all NotSet, StdRebasing, and YieldDelegationTarget accounts equal the rebasingCredits. [^1] > The balanceOf on each account equals `_creditBalances[account] * (alternativeCreditsPerToken[account] > 0 ? alternativeCreditsPerToken[account] : _rebasingCreditsPerToken) - (yieldFrom[account] == 0 ? 0 : _creditBalances[yieldFrom[account]])` @@ -150,9 +154,11 @@ There are four different account types, two of which link to each other behind t ## Rebasing -The token is designed to gently degrade once a huge amount of APY has been earned. Once this crosses a certain point, and enough resolution is no longer possible, transfers should slightly round up. +The token distributes yield to users by "rebasing" (changing supply). This leaves all non-rebasing users with the same account balance. -There is inevitable rounding error when rebasing, since there is no possible way to ensure that totalSupply is exactly matched. Total supply moves up exactly as it is set. +The token is designed to gently degrade in resolutions once a huge amount of APY has been earned. Once this crosses a certain point, and enough resolution is no longer possible, transfers should slightly round up. + +There is inevitable rounding error when rebasing, since there is no possible way to ensure that totalSupply is exactly the result of all the things that make it up. totalSupply must be exactly equal to the set value, nonRebasingSupply does not change, and we handle rounding errors by rounding down the rebasingCreditsPerToken. The resulting gap is distributed to users the next time the token rebases upwards. ## Rebasing invariants @@ -163,8 +169,24 @@ There is inevitable rounding error when rebasing, since there is no possible way > After a call to changeSupply(), the new totalSupply should always match what was passed into the call or the call revert. + +> Only transfers change the balance of `StdNonRebasing` and `YieldDelegationSource` accounts. +## Other invariants + + +After a non-reverting call to `rebaseOptIn()` the `alternativeCreditsPerToken[account] == 0` + + +After a non-reverting call to `rebaseOptOut()` the `alternativeCreditsPerToken[account] == 1e18` + + +A successful mint() call by the vault results in the target account's balance increasing by the amount specified + + +A successful burn() call by the vault results in the target account's balance decreasing by the amount specified + [^1]: From the current code base. Historically there may be different data stored in storage slots. From 59516605c8ba99ed032efd95c494111958ac0988 Mon Sep 17 00:00:00 2001 From: Daniel Von Fange Date: Wed, 13 Nov 2024 13:35:51 -0500 Subject: [PATCH 030/110] Format invarients --- contracts/contracts/token/README-token-logic.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/contracts/token/README-token-logic.md b/contracts/contracts/token/README-token-logic.md index 76ec474274..be47c6f1bb 100644 --- a/contracts/contracts/token/README-token-logic.md +++ b/contracts/contracts/token/README-token-logic.md @@ -176,16 +176,16 @@ There is inevitable rounding error when rebasing, since there is no possible way ## Other invariants -After a non-reverting call to `rebaseOptIn()` the `alternativeCreditsPerToken[account] == 0` +> After a non-reverting call to `rebaseOptIn()` the `alternativeCreditsPerToken[account] == 0` -After a non-reverting call to `rebaseOptOut()` the `alternativeCreditsPerToken[account] == 1e18` +> After a non-reverting call to `rebaseOptOut()` the `alternativeCreditsPerToken[account] == 1e18` -A successful mint() call by the vault results in the target account's balance increasing by the amount specified +> A successful mint() call by the vault results in the target account's balance increasing by the amount specified -A successful burn() call by the vault results in the target account's balance decreasing by the amount specified +> A successful burn() call by the vault results in the target account's balance decreasing by the amount specified [^1]: From the current code base. Historically there may be different data stored in storage slots. From 8c126c8723efe54227fcd973af2e27781a734d2f Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 13 Nov 2024 23:02:42 +0100 Subject: [PATCH 031/110] further simplify the code when handling different rebase states --- contracts/contracts/echidna/OUSDEchidna.sol | 3 +- contracts/contracts/token/OUSD.sol | 61 +++++++++++++-------- 2 files changed, 41 insertions(+), 23 deletions(-) diff --git a/contracts/contracts/echidna/OUSDEchidna.sol b/contracts/contracts/echidna/OUSDEchidna.sol index cca5a6a6f5..60ecaf1bae 100644 --- a/contracts/contracts/echidna/OUSDEchidna.sol +++ b/contracts/contracts/echidna/OUSDEchidna.sol @@ -10,6 +10,7 @@ contract OUSDEchidna is OUSD { public returns (bool) { - return _isNonRebasingAccount(_account); + _autoMigrate(_account); + return alternativeCreditsPerToken[_account] > 0; } } diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index c7632deca2..415601dd77 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -52,7 +52,7 @@ contract OUSD is Governable { uint256 private _rebasingCredits; // Sum of all rebasing credits (_creditBalances for rebasing accounts) uint256 private _rebasingCreditsPerToken; uint256 public nonRebasingSupply; // All nonrebasing balances - mapping(address => uint256) private alternativeCreditsPerToken; + mapping(address => uint256) internal alternativeCreditsPerToken; mapping(address => RebaseOptions) public rebaseState; mapping(address => uint256) public isUpgraded; mapping(address => address) public yieldTo; @@ -305,16 +305,19 @@ contract OUSD is Governable { newCredits.toInt256() - _creditBalances[account].toInt256(); _creditBalances[account] = newCredits; - } else if (_isNonRebasingAccount(account)) { - nonRebasingSupplyDiff = balanceChange; - alternativeCreditsPerToken[account] = 1e18; - _creditBalances[account] = newBalance; } else { - uint256 newCredits = _balanceToRebasingCredits(newBalance); - rebasingCreditsDiff = - newCredits.toInt256() - - _creditBalances[account].toInt256(); - _creditBalances[account] = newCredits; + _autoMigrate(account); + if (alternativeCreditsPerToken[account] > 0) { + nonRebasingSupplyDiff = balanceChange; + alternativeCreditsPerToken[account] = 1e18; + _creditBalances[account] = newBalance; + } else { + uint256 newCredits = _balanceToRebasingCredits(newBalance); + rebasingCreditsDiff = + newCredits.toInt256() - + _creditBalances[account].toInt256(); + _creditBalances[account] = newCredits; + } } } @@ -452,11 +455,26 @@ contract OUSD is Governable { } /** - * @dev Is an account using rebasing accounting or non-rebasing accounting? - * Also, ensure contracts are non-rebasing if they have not opted in. + * @dev Before a `rebaseOptIn` or non yield delegating token `transfer` can be executed contract + * accounts need to have a more explicitly defined rebasing state set. + * + * Contract account can be in the following states before `autoMigrate` is called: + * 1. Under any token contract codebase they haven't been part of any token transfers yet + * having rebaseState `NotSet` and `alternativeCreditsPerToken == 0` + * 2. Under older token contract codebase they have the default rebaseState set to `NotSet` and + * the codebase has "auto-migrated" them by setting the `alternativeCreditsPerToken` to some + * value greater than 0. + * 3. Contract has under any token contract codebase explicitly requested to be opted out of rebasing + * + * Case 1. Needs to be migrated using autoMigrate to a nonRebasing account. + * + * Note: Even with this _autoMigrate function in place there will still be Case 2 accounts existing that + * will behave exactly like RebaseState StdNonRebasing account, and still having their rebase state + * set to `NotSet` + * * @param _account Address of the account. */ - function _isNonRebasingAccount(address _account) internal returns (bool) { + function _autoMigrate(address _account) internal returns (bool) { bool isContract = _account.code.length > 0; // In the older contract implementation: https://github.com/OriginProtocol/origin-dollar/blob/20a21d00a4a6ea9f42940ac194e82655fcda882e/contracts/contracts/token/OUSD.sol#L479-L489 // an account could have non 0 balance, be (or become) a contract with the rebase state @@ -472,10 +490,6 @@ contract OUSD is Governable { ) { _rebaseOptOut(_account); } - - return rebaseState[_account] != RebaseOptions.YieldDelegationSource && - rebaseState[_account] != RebaseOptions.YieldDelegationTarget && - alternativeCreditsPerToken[_account] > 0; } function _balanceToRebasingCredits(uint256 balance) @@ -515,15 +529,17 @@ contract OUSD is Governable { } function _rebaseOptIn(address _account) internal { + _autoMigrate(_account); + require( - _isNonRebasingAccount(_account), + alternativeCreditsPerToken[_account] > 0, "Account must be non-rebasing" ); RebaseOptions state = rebaseState[_account]; require( state == RebaseOptions.StdNonRebasing || - state == RebaseOptions.NotSet, - "Only standard non-rebasing accounts can opt out" + state == RebaseOptions.NotSet, + "Only standard non-rebasing accounts can opt in" ); uint256 balance = balanceOf(msg.sender); @@ -642,15 +658,16 @@ contract OUSD is Governable { "Invalid rebaseState to" ); + if ( - !_isNonRebasingAccount(from) && + alternativeCreditsPerToken[from] == 0 && (stateFrom == RebaseOptions.NotSet || stateFrom == RebaseOptions.StdRebasing) ) { _rebaseOptOut(from); } if ( - _isNonRebasingAccount(to) && + alternativeCreditsPerToken[to] > 0 && (stateTo == RebaseOptions.NotSet || stateTo == RebaseOptions.StdNonRebasing) ) { From 8c2a358a6ba2d3340450bbab655cfc81acbebb12 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 13 Nov 2024 23:13:49 +0100 Subject: [PATCH 032/110] remove nonreentrant modifiers from the toke code as they are not needed --- contracts/contracts/token/OUSD.sol | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 415601dd77..6088eefef9 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -385,7 +385,7 @@ contract OUSD is Governable { * * - `to` cannot be the zero address. */ - function _mint(address _account, uint256 _amount) internal nonReentrant { + function _mint(address _account, uint256 _amount) internal { require(_account != address(0), "Mint to the zero address"); // Account @@ -419,7 +419,7 @@ contract OUSD is Governable { * - `_account` cannot be the zero address. * - `_account` must have at least `_amount` tokens. */ - function _burn(address _account, uint256 _amount) internal nonReentrant { + function _burn(address _account, uint256 _amount) internal { require(_account != address(0), "Burn from the zero address"); if (_amount == 0) { return; @@ -513,7 +513,6 @@ contract OUSD is Governable { */ function governanceRebaseOptIn(address _account) external - nonReentrant onlyGovernor { _rebaseOptIn(_account); @@ -524,7 +523,7 @@ contract OUSD is Governable { * address's balance will be part of rebases and the account will be exposed * to upside and downside. */ - function rebaseOptIn() external nonReentrant { + function rebaseOptIn() external { _rebaseOptIn(msg.sender); } @@ -556,7 +555,7 @@ contract OUSD is Governable { emit AccountRebasingEnabled(_account); } - function rebaseOptOut() external nonReentrant { + function rebaseOptOut() external { _rebaseOptOut(msg.sender); } @@ -594,7 +593,6 @@ contract OUSD is Governable { function changeSupply(uint256 _newTotalSupply) external onlyVault - nonReentrant { require(_totalSupply > 0, "Cannot increase 0 supply"); @@ -631,7 +629,6 @@ contract OUSD is Governable { function delegateYield(address from, address to) external onlyGovernor - nonReentrant { require(from != to, "Cannot delegate to self"); require( @@ -693,7 +690,7 @@ contract OUSD is Governable { _rebasingCredits += credits; } - function undelegateYield(address from) external onlyGovernor nonReentrant { + function undelegateYield(address from) external onlyGovernor { // Require a delegation, which will also ensure a valid delegation require(yieldTo[from] != address(0), ""); From d7a303f14e125b047935cf9e2e64554b4e382598 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Fri, 15 Nov 2024 15:11:55 +0100 Subject: [PATCH 033/110] allow empty conracts to rebaseOptIn without auto migration --- contracts/contracts/token/OUSD.sol | 38 +++++++++++++----------------- contracts/test/token/ousd.js | 22 +++++------------ 2 files changed, 22 insertions(+), 38 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 6088eefef9..96780e4aa4 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -456,22 +456,22 @@ contract OUSD is Governable { /** * @dev Before a `rebaseOptIn` or non yield delegating token `transfer` can be executed contract - * accounts need to have a more explicitly defined rebasing state set. - * + * accounts need to have a more explicitly defined rebasing state set. + * * Contract account can be in the following states before `autoMigrate` is called: * 1. Under any token contract codebase they haven't been part of any token transfers yet * having rebaseState `NotSet` and `alternativeCreditsPerToken == 0` * 2. Under older token contract codebase they have the default rebaseState set to `NotSet` and - * the codebase has "auto-migrated" them by setting the `alternativeCreditsPerToken` to some + * the codebase has "auto-migrated" them by setting the `alternativeCreditsPerToken` to some * value greater than 0. * 3. Contract has under any token contract codebase explicitly requested to be opted out of rebasing * * Case 1. Needs to be migrated using autoMigrate to a nonRebasing account. - * + * * Note: Even with this _autoMigrate function in place there will still be Case 2 accounts existing that * will behave exactly like RebaseState StdNonRebasing account, and still having their rebase state * set to `NotSet` - * + * * @param _account Address of the account. */ function _autoMigrate(address _account) internal returns (bool) { @@ -511,10 +511,7 @@ contract OUSD is Governable { * to upside and downside. * @param _account Address of the account. */ - function governanceRebaseOptIn(address _account) - external - onlyGovernor - { + function governanceRebaseOptIn(address _account) external onlyGovernor { _rebaseOptIn(_account); } @@ -528,13 +525,17 @@ contract OUSD is Governable { } function _rebaseOptIn(address _account) internal { - _autoMigrate(_account); - + // prettier-ignore require( - alternativeCreditsPerToken[_account] > 0, - "Account must be non-rebasing" + alternativeCreditsPerToken[_account] > 0 || ( + _account.code.length > 0 && // isContract + balanceOf(_account) == 0 + ) + , + "Account must be non-rebasing or empty contract" ); RebaseOptions state = rebaseState[_account]; + // prettier-ignore require( state == RebaseOptions.StdNonRebasing || state == RebaseOptions.NotSet, @@ -590,10 +591,7 @@ contract OUSD is Governable { * the exchange rate between "credits" and OUSD tokens to change balances. * @param _newTotalSupply New total supply of OUSD. */ - function changeSupply(uint256 _newTotalSupply) - external - onlyVault - { + function changeSupply(uint256 _newTotalSupply) external onlyVault { require(_totalSupply > 0, "Cannot increase 0 supply"); if (_totalSupply == _newTotalSupply) { @@ -626,10 +624,7 @@ contract OUSD is Governable { ); } - function delegateYield(address from, address to) - external - onlyGovernor - { + function delegateYield(address from, address to) external onlyGovernor { require(from != to, "Cannot delegate to self"); require( yieldFrom[to] == address(0) && @@ -655,7 +650,6 @@ contract OUSD is Governable { "Invalid rebaseState to" ); - if ( alternativeCreditsPerToken[from] == 0 && (stateFrom == RebaseOptions.NotSet || diff --git a/contracts/test/token/ousd.js b/contracts/test/token/ousd.js index 945fd3d329..0c9967bb47 100644 --- a/contracts/test/token/ousd.js +++ b/contracts/test/token/ousd.js @@ -248,9 +248,7 @@ describe("Token", function () { let { ousd, vault, matt, usdc, josh, mockNonRebasing } = fixture; // Give Josh an allowance to move Matt's OUSD - await ousd - .connect(matt) - .approve(await josh.getAddress(), ousdUnits("100")); + await ousd.connect(matt).approve(await josh.getAddress(), ousdUnits("100")); // Give contract 100 OUSD from Matt via Josh await ousd @@ -287,9 +285,7 @@ describe("Token", function () { let { ousd, vault, matt, usdc, josh, mockNonRebasing } = fixture; // Give Josh an allowance to move Matt's OUSD - await ousd - .connect(matt) - .approve(await josh.getAddress(), ousdUnits("150")); + await ousd.connect(matt).approve(await josh.getAddress(), ousdUnits("150")); // Give contract 100 OUSD from Matt via Josh await ousd .connect(josh) @@ -334,10 +330,7 @@ describe("Token", function () { await expect(matt).has.an.approxBalanceOf("100.00", ousd); await expect(josh).has.an.approxBalanceOf("0", ousd); await expect(mockNonRebasing).has.an.approxBalanceOf("100.00", ousd); - await mockNonRebasing.approve( - await matt.getAddress(), - ousdUnits("100") - ); + await mockNonRebasing.approve(await matt.getAddress(), ousdUnits("100")); await ousd .connect(matt) @@ -381,10 +374,7 @@ describe("Token", function () { await expect(matt).has.an.approxBalanceOf("250", ousd); await expect(mockNonRebasing).has.an.approxBalanceOf("150.00", ousd); // Transfer contract balance to Josh - await mockNonRebasing.approve( - await matt.getAddress(), - ousdUnits("150") - ); + await mockNonRebasing.approve(await matt.getAddress(), ousdUnits("150")); await ousd .connect(matt) @@ -502,7 +492,7 @@ describe("Token", function () { it("Should not allow EOA to call rebaseOptIn when already opted in to rebasing", async () => { let { ousd, matt } = fixture; await expect(ousd.connect(matt).rebaseOptIn()).to.be.revertedWith( - "Account must be non-rebasing" + "Account must be non-rebasing or empty contract" ); }); @@ -518,7 +508,7 @@ describe("Token", function () { let { mockNonRebasing } = fixture; await mockNonRebasing.rebaseOptIn(); await expect(mockNonRebasing.rebaseOptIn()).to.be.revertedWith( - "Account must be non-rebasing" + "Only standard non-rebasing accounts can opt in" ); }); From 2b3189f39c89ab01e34e2f0a8e0a1fa6bf079c96 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Fri, 15 Nov 2024 15:15:50 +0100 Subject: [PATCH 034/110] Transfer unit tests for new token implementation (#2310) * remove nonreentrant modifiers from the toke code as they are not needed * add setup for all account types * check all relevant contract initial states * add test between any possible contract accounts * prettier * add some documentation and prettier --- .../contracts/mocks/TestUpgradedOUSD.sol | 25 ++ contracts/contracts/token/OUSD.sol | 4 +- contracts/deploy/deployActions.js | 8 +- contracts/test/_fixture.js | 383 ++++++++++++++++-- contracts/test/token/ousd.js | 4 +- contracts/test/token/token-transfers.js | 230 +++++++++++ 6 files changed, 608 insertions(+), 46 deletions(-) create mode 100644 contracts/contracts/mocks/TestUpgradedOUSD.sol create mode 100644 contracts/test/token/token-transfers.js diff --git a/contracts/contracts/mocks/TestUpgradedOUSD.sol b/contracts/contracts/mocks/TestUpgradedOUSD.sol new file mode 100644 index 0000000000..9d3555f9a2 --- /dev/null +++ b/contracts/contracts/mocks/TestUpgradedOUSD.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "../token/OUSD.sol"; + +// used to alter internal state of OUSD contract +contract TestUpgradedOUSD is OUSD { + constructor() OUSD() {} + + function overwriteCreditBalances(address _account, uint256 _creditBalance) + public + { + _creditBalances[_account] = _creditBalance; + } + + function overwriteAlternativeCPT(address _account, uint256 _acpt) public { + alternativeCreditsPerToken[_account] = _acpt; + } + + function overwriteRebaseState(address _account, RebaseOptions _rebaseOption) + public + { + rebaseState[_account] = _rebaseOption; + } +} diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 96780e4aa4..dead3a4f93 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -48,7 +48,7 @@ contract OUSD is Governable { uint256 public _totalSupply; mapping(address => mapping(address => uint256)) private _allowances; address public vaultAddress = address(0); - mapping(address => uint256) private _creditBalances; + mapping(address => uint256) internal _creditBalances; uint256 private _rebasingCredits; // Sum of all rebasing credits (_creditBalances for rebasing accounts) uint256 private _rebasingCreditsPerToken; uint256 public nonRebasingSupply; // All nonrebasing balances @@ -538,7 +538,7 @@ contract OUSD is Governable { // prettier-ignore require( state == RebaseOptions.StdNonRebasing || - state == RebaseOptions.NotSet, + state == RebaseOptions.NotSet, "Only standard non-rebasing accounts can opt in" ); diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index 2c6d7d5a1e..29a136b8a6 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -6,6 +6,7 @@ const { getOracleAddresses, isMainnet, isHolesky, + isTest, } = require("../test/helpers.js"); const { deployWithConfirmation, withConfirmation } = require("../utils/deploy"); const { @@ -1190,7 +1191,12 @@ const deployOUSDCore = async () => { await deployWithConfirmation("VaultProxy"); // Main contracts - const dOUSD = await deployWithConfirmation("OUSD"); + let dOUSD; + if (isTest) { + dOUSD = await deployWithConfirmation("TestUpgradedOUSD"); + } else { + dOUSD = await deployWithConfirmation("OUSD"); + } const dVault = await deployWithConfirmation("Vault"); const dVaultCore = await deployWithConfirmation("VaultCore"); const dVaultAdmin = await deployWithConfirmation("VaultAdmin"); diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index 908eea9f95..3f06329f9c 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -20,6 +20,7 @@ const { fundAccounts, fundAccountsForOETHUnitTests, } = require("../utils/funding"); + const { replaceContractAt } = require("../utils/hardhat"); const { getAssetAddresses, @@ -189,11 +190,297 @@ const simpleOETHFixture = deployments.createFixture(async () => { }; }); -const defaultFixture = deployments.createFixture(async () => { - if (!snapshotId && !isFork) { - snapshotId = await nodeSnapshot(); +const getVaultAndTokenConracts = async () => { + const ousdProxy = await ethers.getContract("OUSDProxy"); + const vaultProxy = await ethers.getContract("VaultProxy"); + + const ousd = await ethers.getContractAt("OUSD", ousdProxy.address); + // the same contract as the "ousd" one just with some unlocked features + const ousdUnlocked = await ethers.getContractAt( + "TestUpgradedOUSD", + ousdProxy.address + ); + + const vault = await ethers.getContractAt("IVault", vaultProxy.address); + + const oethProxy = await ethers.getContract("OETHProxy"); + const OETHVaultProxy = await ethers.getContract("OETHVaultProxy"); + const oethVault = await ethers.getContractAt( + "IVault", + OETHVaultProxy.address + ); + const oeth = await ethers.getContractAt("OETH", oethProxy.address); + + let woeth, woethProxy, mockNonRebasing, mockNonRebasingTwo; + + if (isFork) { + woethProxy = await ethers.getContract("WOETHProxy"); + woeth = await ethers.getContractAt("WOETH", woethProxy.address); + } else { + // Mock contracts for testing rebase opt out + mockNonRebasing = await ethers.getContract("MockNonRebasing"); + await mockNonRebasing.setOUSD(ousd.address); + mockNonRebasingTwo = await ethers.getContract("MockNonRebasingTwo"); + await mockNonRebasingTwo.setOUSD(ousd.address); } + return { + ousd, + ousdUnlocked, + vault, + oethVault, + oeth, + woeth, + mockNonRebasing, + mockNonRebasingTwo, + }; +}; + +/** + * This fixture creates the 4 different OUSD contract account types in all of + * the possible storage configuration: StdRebasing, StdNonRebasing, YieldDelegationSource, + * YieldDelegationTarget + */ +const createAccountTypes = async ({ vault, ousd, ousdUnlocked, deploy }) => { + const signers = await hre.ethers.getSigners(); + const matt = signers[4]; + const governor = signers[1]; + + if (!isFork) { + await fundAccounts(); + const dai = await ethers.getContract("MockDAI"); + await dai.connect(matt).approve(vault.address, daiUnits("1000")); + await vault.connect(matt).mint(dai.address, daiUnits("1000"), 0); + } + + // yiedlsource + // yieldtarget + + const createAccount = async () => { + let account = ethers.Wallet.createRandom(); + // Give ETH to user + await hardhatSetBalance(account.address, "1000000"); + account = account.connect(ethers.provider); + return account; + }; + + const createContract = async (name) => { + const fullName = `MockNonRebasing_${name}`; + await deploy(fullName, { + from: matt.address, + contract: "MockNonRebasing", + }); + + const contract = await ethers.getContract(fullName); + await contract.setOUSD(ousd.address); + + return contract; + }; + + // generate alternativeCreditsPerToken BigNumber and creditBalance BigNumber + // for a given credits per token + const generateCreditsBalancePair = ({ creditsPerToken, tokenBalance }) => { + const creditsPerTokenBN = parseUnits(`${creditsPerToken}`, 27); + // 1e18 * 1e27 / 1e18 + const creditsBalanceBN = tokenBalance + .mul(creditsPerTokenBN) + .div(parseUnits("1", 18)); + + return { + creditsPerTokenBN, + creditsBalanceBN, + }; + }; + + const createNonRebasingNotSetAlternativeCptContract = async ({ + name, + creditsPerToken, + balance, + }) => { + const nonrebase_cotract_notSet_altcpt_gt = await createContract(name); + await ousd + .connect(matt) + .transfer(nonrebase_cotract_notSet_altcpt_gt.address, balance); + const { creditsPerTokenBN, creditsBalanceBN } = generateCreditsBalancePair({ + creditsPerToken, + tokenBalance: balance, + }); + await ousdUnlocked + .connect(matt) + .overwriteCreditBalances( + nonrebase_cotract_notSet_altcpt_gt.address, + creditsBalanceBN + ); + await ousdUnlocked + .connect(matt) + .overwriteAlternativeCPT( + nonrebase_cotract_notSet_altcpt_gt.address, + creditsPerTokenBN + ); + await ousdUnlocked.connect(matt).overwriteRebaseState( + nonrebase_cotract_notSet_altcpt_gt.address, + 0 // NotSet + ); + + return nonrebase_cotract_notSet_altcpt_gt; + }; + + const rebase_eoa_notset_0 = await createAccount(); + await ousd + .connect(matt) + .transfer(rebase_eoa_notset_0.address, ousdUnits("11")); + const rebase_eoa_notset_1 = await createAccount(); + await ousd + .connect(matt) + .transfer(rebase_eoa_notset_1.address, ousdUnits("12")); + + const rebase_eoa_stdRebasing_0 = await createAccount(); + await ousd + .connect(matt) + .transfer(rebase_eoa_stdRebasing_0.address, ousdUnits("21")); + await ousd.connect(rebase_eoa_stdRebasing_0).rebaseOptOut(); + await ousd.connect(rebase_eoa_stdRebasing_0).rebaseOptIn(); + const rebase_eoa_stdRebasing_1 = await createAccount(); + await ousd + .connect(matt) + .transfer(rebase_eoa_stdRebasing_1.address, ousdUnits("22")); + await ousd.connect(rebase_eoa_stdRebasing_1).rebaseOptOut(); + await ousd.connect(rebase_eoa_stdRebasing_1).rebaseOptIn(); + + const rebase_contract_0 = await createContract("rebase_contract_0"); + await ousd.connect(matt).transfer(rebase_contract_0.address, ousdUnits("33")); + await rebase_contract_0.connect(matt).rebaseOptIn(); + const rebase_contract_1 = await createContract("rebase_contract_1"); + await ousd.connect(matt).transfer(rebase_contract_1.address, ousdUnits("34")); + await rebase_contract_1.connect(matt).rebaseOptIn(); + + const nonrebase_eoa_0 = await createAccount(); + await ousd.connect(matt).transfer(nonrebase_eoa_0.address, ousdUnits("44")); + await ousd.connect(nonrebase_eoa_0).rebaseOptOut(); + const nonrebase_eoa_1 = await createAccount(); + await ousd.connect(matt).transfer(nonrebase_eoa_1.address, ousdUnits("45")); + await ousd.connect(nonrebase_eoa_1).rebaseOptOut(); + + const nonrebase_cotract_0 = await createContract("nonrebase_cotract_0"); + await ousd + .connect(matt) + .transfer(nonrebase_cotract_0.address, ousdUnits("55")); + await nonrebase_cotract_0.connect(matt).rebaseOptIn(); + await nonrebase_cotract_0.connect(matt).rebaseOptOut(); + const nonrebase_cotract_1 = await createContract("nonrebase_cotract_1"); + await ousd + .connect(matt) + .transfer(nonrebase_cotract_1.address, ousdUnits("56")); + await nonrebase_cotract_1.connect(matt).rebaseOptIn(); + await nonrebase_cotract_1.connect(matt).rebaseOptOut(); + + const nonrebase_cotract_notSet_0 = await createContract( + "nonrebase_cotract_notSet_0" + ); + const nonrebase_cotract_notSet_1 = await createContract( + "nonrebase_cotract_notSet_1" + ); + + const nonrebase_cotract_notSet_altcpt_gt_0 = + await createNonRebasingNotSetAlternativeCptContract({ + name: "nonrebase_cotract_notSet_altcpt_gt_0", + creditsPerToken: 0.934232, + balance: ousdUnits("65"), + }); + + const nonrebase_cotract_notSet_altcpt_gt_1 = + await createNonRebasingNotSetAlternativeCptContract({ + name: "nonrebase_cotract_notSet_altcpt_gt_1", + creditsPerToken: 0.890232, + balance: ousdUnits("66"), + }); + + const rebase_delegate_source_0 = await createAccount(); + await ousd + .connect(matt) + .transfer(rebase_delegate_source_0.address, ousdUnits("76")); + const rebase_delegate_target_0 = await createAccount(); + await ousd + .connect(matt) + .transfer(rebase_delegate_target_0.address, ousdUnits("77")); + + await ousd + .connect(governor) + .delegateYield( + rebase_delegate_source_0.address, + rebase_delegate_target_0.address + ); + + const rebase_delegate_source_1 = await createAccount(); + await ousd + .connect(matt) + .transfer(rebase_delegate_source_1.address, ousdUnits("87")); + const rebase_delegate_target_1 = await createAccount(); + await ousd + .connect(matt) + .transfer(rebase_delegate_target_1.address, ousdUnits("88")); + + await ousd + .connect(governor) + .delegateYield( + rebase_delegate_source_1.address, + rebase_delegate_target_1.address + ); + + // matt burn remaining OUSD + await vault.connect(matt).redeemAll(ousdUnits("0")); + + return { + // StdRebasing account type: + // - all have alternativeCreditsPerToken = 0 + // - _creditBalances non zero using global contract's rebasingCredits to compute balance + + // EOA account that has rebaseState: NotSet + rebase_eoa_notset_0, + rebase_eoa_notset_1, + // EOA account that has rebaseState: StdRebasing + rebase_eoa_stdRebasing_0, + rebase_eoa_stdRebasing_1, + // contract account that has rebaseState: StdRebasing + rebase_contract_0, + rebase_contract_1, + + // StdNonRebasing account type: + // - alternativeCreditsPerToken > 0 & 1e18 for new accounts + // - _creditBalances non zero: + // - new accounts match _creditBalances to their token balance + // - older accounts use _creditBalances & alternativeCreditsPerToken to compute token balance + + // EOA account that has rebaseState: StdNonRebasing + nonrebase_eoa_0, + nonrebase_eoa_1, + // contract account that has rebaseState: StdNonRebasing + nonrebase_cotract_0, + nonrebase_cotract_1, + // contract account that has rebaseState: NotSet + nonrebase_cotract_notSet_0, + nonrebase_cotract_notSet_1, + // contract account that has rebaseState: NotSet & alternativeCreditsPerToken > 0 + // note: these are older accounts that have been migrated by the older versions of + // of the code without explicitly setting rebaseState to StdNonRebasing + nonrebase_cotract_notSet_altcpt_gt_0, + nonrebase_cotract_notSet_altcpt_gt_1, + + // account delegating yield + rebase_delegate_source_0, + rebase_delegate_source_1, + + // account receiving delegated yield + rebase_delegate_target_0, + rebase_delegate_target_1, + }; +}; + +/** + * Vault and token fixture with extra functionality regarding different types of accounts + * (rebaseStates and alternativeCreditsPerToken ) when testing token contract behaviour + */ +const loadTokenTransferFixture = deployments.createFixture(async () => { log(`Forked from block: ${await hre.ethers.provider.getBlockNumber()}`); log(`Before deployments with param "${isFork ? undefined : ["unit_tests"]}"`); @@ -209,30 +496,46 @@ const defaultFixture = deployments.createFixture(async () => { const { governorAddr, strategistAddr, timelockAddr } = await getNamedAccounts(); - const ousdProxy = await ethers.getContract("OUSDProxy"); - const vaultProxy = await ethers.getContract("VaultProxy"); + const vaultAndTokenConracts = await getVaultAndTokenConracts(); - const compoundStrategyProxy = await ethers.getContract( - "CompoundStrategyProxy" - ); + const accountTypes = await createAccountTypes({ + ousd: vaultAndTokenConracts.ousd, + ousdUnlocked: vaultAndTokenConracts.ousdUnlocked, + vault: vaultAndTokenConracts.vault, + deploy: deployments.deploy, + governor: deployments.governor, + }); - const ousd = await ethers.getContractAt("OUSD", ousdProxy.address); - const vault = await ethers.getContractAt("IVault", vaultProxy.address); + return { + ...vaultAndTokenConracts, + ...accountTypes, + governorAddr, + strategistAddr, + timelockAddr, + }; +}); - const oethProxy = await ethers.getContract("OETHProxy"); - const OETHVaultProxy = await ethers.getContract("OETHVaultProxy"); - const oethVault = await ethers.getContractAt( - "IVault", - OETHVaultProxy.address - ); - const oeth = await ethers.getContractAt("OETH", oethProxy.address); +const defaultFixture = deployments.createFixture(async () => { + if (!snapshotId && !isFork) { + snapshotId = await nodeSnapshot(); + } - let woeth, woethProxy; + log(`Forked from block: ${await hre.ethers.provider.getBlockNumber()}`); - if (isFork) { - woethProxy = await ethers.getContract("WOETHProxy"); - woeth = await ethers.getContractAt("WOETH", woethProxy.address); - } + log(`Before deployments with param "${isFork ? undefined : ["unit_tests"]}"`); + + // Run the contract deployments + await deployments.fixture(isFork ? undefined : ["unit_tests"], { + keepExistingDeployments: true, + fallbackToGlobal: true, + }); + + log(`Block after deployments: ${await hre.ethers.provider.getBlockNumber()}`); + + const { governorAddr, strategistAddr, timelockAddr } = + await getNamedAccounts(); + + const vaultAndTokenConracts = await getVaultAndTokenConracts(); const harvesterProxy = await ethers.getContract("HarvesterProxy"); const harvester = await ethers.getContractAt( @@ -253,6 +556,11 @@ const defaultFixture = deployments.createFixture(async () => { const CompoundStrategyFactory = await ethers.getContractFactory( "CompoundStrategy" ); + + const compoundStrategyProxy = await ethers.getContract( + "CompoundStrategyProxy" + ); + const compoundStrategy = await ethers.getContractAt( "CompoundStrategy", compoundStrategyProxy.address @@ -341,8 +649,6 @@ const defaultFixture = deployments.createFixture(async () => { sfrxETH, sDAI, usdcMetaMorphoSteakHouseVault, - mockNonRebasing, - mockNonRebasingTwo, LUSD, fdai, fusdt, @@ -613,12 +919,6 @@ const defaultFixture = deployments.createFixture(async () => { "MockChainlinkOracleFeedETH" ); - // Mock contracts for testing rebase opt out - mockNonRebasing = await ethers.getContract("MockNonRebasing"); - await mockNonRebasing.setOUSD(ousd.address); - mockNonRebasingTwo = await ethers.getContract("MockNonRebasingTwo"); - await mockNonRebasingTwo.setOUSD(ousd.address); - flipper = await ethers.getContract("Flipper"); const LUSDMetaStrategyProxy = await ethers.getContract( @@ -649,10 +949,12 @@ const defaultFixture = deployments.createFixture(async () => { const sGovernor = await ethers.provider.getSigner(governorAddr); // Add TUSD in fixture, it is disabled by default in deployment - await vault.connect(sGovernor).supportAsset(assetAddresses.TUSD, 0); + await vaultAndTokenConracts.vault + .connect(sGovernor) + .supportAsset(assetAddresses.TUSD, 0); // Enable capital movement - await vault.connect(sGovernor).unpauseCapital(); + await vaultAndTokenConracts.vault.connect(sGovernor).unpauseCapital(); } const signers = await hre.ethers.getSigners(); @@ -678,11 +980,16 @@ const defaultFixture = deployments.createFixture(async () => { // Matt and Josh each have $100 OUSD for (const user of [matt, josh]) { - await dai.connect(user).approve(vault.address, daiUnits("100")); - await vault.connect(user).mint(dai.address, daiUnits("100"), 0); + await dai + .connect(user) + .approve(vaultAndTokenConracts.vault.address, daiUnits("100")); + await vaultAndTokenConracts.vault + .connect(user) + .mint(dai.address, daiUnits("100"), 0); } } return { + ...vaultAndTokenConracts, // Accounts matt, josh, @@ -696,13 +1003,9 @@ const defaultFixture = deployments.createFixture(async () => { timelock, oldTimelock, // Contracts - ousd, - vault, vaultValueChecker, harvester, dripper, - mockNonRebasing, - mockNonRebasingTwo, // Oracle chainlinkOracleFeedDAI, chainlinkOracleFeedUSDT, @@ -779,9 +1082,7 @@ const defaultFixture = deployments.createFixture(async () => { fusdt, // OETH - oethVault, oethVaultValueChecker, - oeth, frxETH, sfrxETH, sDAI, @@ -792,7 +1093,6 @@ const defaultFixture = deployments.createFixture(async () => { lidoWithdrawalStrategy, balancerREthStrategy, oethMorphoAaveStrategy, - woeth, convexEthMetaStrategy, oethDripper, oethHarvester, @@ -2540,6 +2840,7 @@ module.exports = { resetAllowance, defaultFixture, oethDefaultFixture, + loadTokenTransferFixture, mockVaultFixture, compoundFixture, compoundVaultFixture, diff --git a/contracts/test/token/ousd.js b/contracts/test/token/ousd.js index 0c9967bb47..996af5f481 100644 --- a/contracts/test/token/ousd.js +++ b/contracts/test/token/ousd.js @@ -786,7 +786,7 @@ describe("Token", function () { const beforeReceiver = await ousd.balanceOf(mockNonRebasing.address); await ousd.connect(matt).transfer(mockNonRebasing.address, amount); const afterReceiver = await ousd.balanceOf(mockNonRebasing.address); - expect(beforeReceiver.add(amount)).to.equal(afterReceiver); + await expect(beforeReceiver.add(amount)).to.equal(afterReceiver); }; // Helper to verify balance-exact transfers out @@ -794,7 +794,7 @@ describe("Token", function () { const beforeReceiver = await ousd.balanceOf(mockNonRebasing.address); await mockNonRebasing.transfer(matt.address, amount); const afterReceiver = await ousd.balanceOf(mockNonRebasing.address); - expect(beforeReceiver.sub(amount)).to.equal(afterReceiver); + await expect(beforeReceiver.sub(amount)).to.equal(afterReceiver); }; // In diff --git a/contracts/test/token/token-transfers.js b/contracts/test/token/token-transfers.js new file mode 100644 index 0000000000..2c1dd9bb9f --- /dev/null +++ b/contracts/test/token/token-transfers.js @@ -0,0 +1,230 @@ +const { expect } = require("chai"); +const { loadTokenTransferFixture } = require("../_fixture"); + +const { isFork, ousdUnits } = require("../helpers"); + +describe("Token Transfers", function () { + if (isFork) { + this.timeout(0); + } + let fixture; + beforeEach(async () => { + fixture = await loadTokenTransferFixture(); + }); + + it("Accounts and ousd contract should have correct initial states", async () => { + const { + rebase_eoa_notset_0, + rebase_eoa_notset_1, + rebase_eoa_stdRebasing_0, + rebase_eoa_stdRebasing_1, + rebase_contract_0, + rebase_contract_1, + nonrebase_eoa_0, + nonrebase_eoa_1, + nonrebase_cotract_0, + nonrebase_cotract_1, + nonrebase_cotract_notSet_0, + nonrebase_cotract_notSet_1, + nonrebase_cotract_notSet_altcpt_gt_0, + nonrebase_cotract_notSet_altcpt_gt_1, + rebase_delegate_source_0, + rebase_delegate_source_1, + rebase_delegate_target_0, + rebase_delegate_target_1, + ousd, + } = fixture; + + expect(await ousd.rebaseState(rebase_eoa_notset_0.address)).to.equal(0); // rebaseState:NotSet + await expect(rebase_eoa_notset_0).has.a.balanceOf("11", ousd); + expect(await ousd.rebaseState(rebase_eoa_notset_1.address)).to.equal(0); // rebaseState:NotSet + await expect(rebase_eoa_notset_1).has.a.balanceOf("12", ousd); + + expect(await ousd.rebaseState(rebase_eoa_stdRebasing_0.address)).to.equal( + 2 + ); // rebaseState:StdRebasing + await expect(rebase_eoa_stdRebasing_0).has.a.balanceOf("21", ousd); + expect(await ousd.rebaseState(rebase_eoa_stdRebasing_1.address)).to.equal( + 2 + ); // rebaseState:StdRebasing + await expect(rebase_eoa_stdRebasing_1).has.a.balanceOf("22", ousd); + + expect(await ousd.rebaseState(rebase_contract_0.address)).to.equal(2); // rebaseState:StdRebasing + await expect(rebase_contract_0).has.a.balanceOf("33", ousd); + expect(await ousd.rebaseState(rebase_contract_1.address)).to.equal(2); // rebaseState:StdRebasing + await expect(rebase_contract_1).has.a.balanceOf("34", ousd); + + expect(await ousd.rebaseState(nonrebase_eoa_0.address)).to.equal(1); // rebaseState:StdNonRebasing + await expect(nonrebase_eoa_0).has.a.balanceOf("44", ousd); + expect(await ousd.rebaseState(nonrebase_eoa_1.address)).to.equal(1); // rebaseState:StdNonRebasing + await expect(nonrebase_eoa_1).has.a.balanceOf("45", ousd); + + expect(await ousd.rebaseState(nonrebase_cotract_0.address)).to.equal(1); // rebaseState:StdNonRebasing + await expect(nonrebase_cotract_0).has.a.balanceOf("55", ousd); + expect(await ousd.rebaseState(nonrebase_cotract_1.address)).to.equal(1); // rebaseState:StdNonRebasing + await expect(nonrebase_cotract_1).has.a.balanceOf("56", ousd); + + expect(await ousd.rebaseState(nonrebase_cotract_notSet_0.address)).to.equal( + 0 + ); // rebaseState:NotSet + await expect(nonrebase_cotract_notSet_0).has.a.balanceOf("0", ousd); + expect(await ousd.rebaseState(nonrebase_cotract_notSet_1.address)).to.equal( + 0 + ); // rebaseState:NotSet + await expect(nonrebase_cotract_notSet_1).has.a.balanceOf("0", ousd); + + expect( + await ousd.rebaseState(nonrebase_cotract_notSet_altcpt_gt_0.address) + ).to.equal(0); // rebaseState:NotSet + await expect(nonrebase_cotract_notSet_altcpt_gt_0).has.a.balanceOf( + "65", + ousd + ); + expect( + await ousd.rebaseState(nonrebase_cotract_notSet_altcpt_gt_1.address) + ).to.equal(0); // rebaseState:NotSet + await expect(nonrebase_cotract_notSet_altcpt_gt_1).has.a.balanceOf( + "66", + ousd + ); + + expect(await ousd.rebaseState(rebase_delegate_source_0.address)).to.equal( + 3 + ); // rebaseState:YieldDelegationSource + await expect(rebase_delegate_source_0).has.a.balanceOf("76", ousd); + expect(await ousd.rebaseState(rebase_delegate_source_1.address)).to.equal( + 3 + ); // rebaseState:YieldDelegationSource + await expect(rebase_delegate_source_1).has.a.balanceOf("87", ousd); + + expect(await ousd.rebaseState(rebase_delegate_target_0.address)).to.equal( + 4 + ); // rebaseState:YieldDelegationTarget + await expect(rebase_delegate_target_0).has.a.balanceOf("77", ousd); + expect(await ousd.rebaseState(rebase_delegate_target_1.address)).to.equal( + 4 + ); // rebaseState:YieldDelegationTarget + await expect(rebase_delegate_target_1).has.a.balanceOf("88", ousd); + + // prettier-ignore + const totalSupply = 11 + 12 + 21 + 22 + 33 + 34 + 44 + + 45 + 55 + 56 + 65 + 66 + 76 + 87 + 77 + 88; + const nonRebasingSupply = 44 + 45 + 55 + 56 + 65 + 66; + expect(await ousd.totalSupply()).to.equal(ousdUnits(`${totalSupply}`)); + expect(await ousd.nonRebasingSupply()).to.equal( + ousdUnits(`${nonRebasingSupply}`) + ); + }); + + const fromAccounts = [ + { + name: "rebase_eoa_notset_0", + affectsRebasingCredits: true, + isContract: false, + }, + { + name: "rebase_eoa_stdRebasing_0", + affectsRebasingCredits: true, + isContract: false, + }, + { + name: "rebase_contract_0", + affectsRebasingCredits: true, + isContract: true, + }, + { + name: "nonrebase_eoa_0", + affectsRebasingCredits: false, + isContract: false, + }, + { + name: "nonrebase_cotract_0", + affectsRebasingCredits: false, + isContract: true, + }, + // can not initiate a transfer from below contract since it has the balance of 0 + //{name: "nonrebase_cotract_notSet_0", affectsRebasingCredits: false, isContract: true}, + { + name: "nonrebase_cotract_notSet_altcpt_gt_0", + affectsRebasingCredits: false, + isContract: true, + }, + { + name: "rebase_delegate_source_0", + affectsRebasingCredits: true, + isContract: false, + }, + { + name: "rebase_delegate_target_0", + affectsRebasingCredits: true, + isContract: false, + }, + ]; + + const toAccounts = [ + { name: "rebase_eoa_notset_1", affectsRebasingCredits: true }, + { name: "rebase_eoa_stdRebasing_1", affectsRebasingCredits: true }, + { name: "rebase_contract_1", affectsRebasingCredits: true }, + { name: "nonrebase_eoa_1", affectsRebasingCredits: false }, + { name: "nonrebase_cotract_1", affectsRebasingCredits: false }, + { name: "nonrebase_cotract_notSet_1", affectsRebasingCredits: false }, + { + name: "nonrebase_cotract_notSet_altcpt_gt_1", + affectsRebasingCredits: false, + }, + { name: "rebase_delegate_source_1", affectsRebasingCredits: true }, + { name: "rebase_delegate_target_1", affectsRebasingCredits: true }, + ]; + + const totalSupply = ousdUnits("792"); + const nonRebasingSupply = ousdUnits("331"); + for (let i = 0; i < fromAccounts.length; i++) { + for (let j = 0; j < toAccounts.length; j++) { + const { + name: fromName, + affectsRebasingCredits: fromAffectsRC, + isContract, + } = fromAccounts[i]; + const { name: toName, affectsRebasingCredits: toAffectsRC } = + toAccounts[j]; + + it(`Should transfer from ${fromName} to ${toName}`, async () => { + const fromAccount = fixture[fromName]; + const toAccount = fixture[toName]; + const { ousd } = fixture; + + const fromBalance = await ousd.balanceOf(fromAccount.address); + const toBalance = await ousd.balanceOf(toAccount.address); + // Random transfer between 2-8 + const amount = ousdUnits(`${2 + Math.random() * 6}`); + + if (isContract) { + await fromAccount.transfer(toAccount.address, amount); + } else { + await ousd.connect(fromAccount).transfer(toAccount.address, amount); + } + + // check balances + await expect(await ousd.balanceOf(fromAccount.address)).to.equal( + fromBalance.sub(amount) + ); + await expect(await ousd.balanceOf(toAccount.address)).to.equal( + toBalance.add(amount) + ); + + let expectedNonRebasingSupply = nonRebasingSupply; + if (!fromAffectsRC) { + expectedNonRebasingSupply = expectedNonRebasingSupply.sub(amount); + } + if (!toAffectsRC) { + expectedNonRebasingSupply = expectedNonRebasingSupply.add(amount); + } + // check global contract (in)variants + await expect(await ousd.totalSupply()).to.equal(totalSupply); + await expect(await ousd.nonRebasingSupply()).to.equal( + expectedNonRebasingSupply + ); + }); + } + } +}); From aa53ea3baba1b0b9ea0060e6e333e920a0e533d4 Mon Sep 17 00:00:00 2001 From: Daniel Von Fange Date: Wed, 13 Nov 2024 13:52:18 -0500 Subject: [PATCH 035/110] More invarients --- contracts/contracts/token/README-token-logic.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/contracts/contracts/token/README-token-logic.md b/contracts/contracts/token/README-token-logic.md index be47c6f1bb..4943e77642 100644 --- a/contracts/contracts/token/README-token-logic.md +++ b/contracts/contracts/token/README-token-logic.md @@ -52,7 +52,9 @@ Transitions to: ### StdNonRebasing Account (Default) -This account does not earn yield. It was originally created for backwards compatibility with systems that did not support balance changes, as well not wasting yield on third party contracts holding tokens that did not support any distribution to users. As a side benefit, regular users earn at a higher rate than the increase in assets. +This account does not earn yield. It was originally created for backwards compatibility with contracts that did not support non-transfer balance changes, as well as to not waste giving yield to third party contracts that did not support any yield distribution to users. + +As a side benefit, because of these contracts, regular users earn at a higher rate than they would otherwise get. Reads: @@ -158,7 +160,7 @@ The token distributes yield to users by "rebasing" (changing supply). This leave The token is designed to gently degrade in resolutions once a huge amount of APY has been earned. Once this crosses a certain point, and enough resolution is no longer possible, transfers should slightly round up. -There is inevitable rounding error when rebasing, since there is no possible way to ensure that totalSupply is exactly the result of all the things that make it up. totalSupply must be exactly equal to the set value, nonRebasingSupply does not change, and we handle rounding errors by rounding down the rebasingCreditsPerToken. The resulting gap is distributed to users the next time the token rebases upwards. +There is inevitable rounding error when rebasing, since there is no possible way to ensure that totalSupply is exactly the result of all the things that make it up. This is because totalSupply must be exactly equal to the new value and nonRebasingSupply must not change. The only option is to handle rounding errors by rounding down the rebasingCreditsPerToken. The resulting gap of undistributed yield is later distributed to users the next time the token rebases upwards. ## Rebasing invariants @@ -167,7 +169,7 @@ There is inevitable rounding error when rebasing, since there is no possible way > After a call to changeSupply() then `nonRebasingCredits + (rebasingCredits / rebasingCreditsPer) <= totalSupply` -> After a call to changeSupply(), the new totalSupply should always match what was passed into the call or the call revert. +> After a non-reverting call to changeSupply(), the new totalSupply should always match what was passed into the call. > Only transfers change the balance of `StdNonRebasing` and `YieldDelegationSource` accounts. @@ -178,9 +180,18 @@ There is inevitable rounding error when rebasing, since there is no possible way > After a non-reverting call to `rebaseOptIn()` the `alternativeCreditsPerToken[account] == 0` + +> Calling `rebaseOptIn()` does not result in a change in account balance. [^2] + > After a non-reverting call to `rebaseOptOut()` the `alternativeCreditsPerToken[account] == 1e18` + +> Calling `rebaseOptOut()` does not result in a change in account balance. + + +> Only `transfer`, `transferFrom`, `mint`, `burn`, and `changeSupply` result in a change in any account's balance. + > A successful mint() call by the vault results in the target account's balance increasing by the amount specified From 727e4e908b765e20631a9ed1bd16d6d58ea18e87 Mon Sep 17 00:00:00 2001 From: Daniel Von Fange Date: Fri, 15 Nov 2024 09:26:57 -0500 Subject: [PATCH 036/110] Correct total supply docs --- contracts/contracts/token/README-token-logic.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/contracts/contracts/token/README-token-logic.md b/contracts/contracts/token/README-token-logic.md index 4943e77642..5526abf134 100644 --- a/contracts/contracts/token/README-token-logic.md +++ b/contracts/contracts/token/README-token-logic.md @@ -125,8 +125,7 @@ Transitions to: > Any account with `rebaseState` = `YieldDelegationTarget` has a nonZero `yieldFrom` -> Any non zero `YieldFrom` points to an account that has a `YieldTo` pointing back to it - +> Any non zero valued `YieldFrom` points to an account that has a `YieldTo` pointing back to the starting account. ## Balance Invariants @@ -160,8 +159,6 @@ The token distributes yield to users by "rebasing" (changing supply). This leave The token is designed to gently degrade in resolutions once a huge amount of APY has been earned. Once this crosses a certain point, and enough resolution is no longer possible, transfers should slightly round up. -There is inevitable rounding error when rebasing, since there is no possible way to ensure that totalSupply is exactly the result of all the things that make it up. This is because totalSupply must be exactly equal to the new value and nonRebasingSupply must not change. The only option is to handle rounding errors by rounding down the rebasingCreditsPerToken. The resulting gap of undistributed yield is later distributed to users the next time the token rebases upwards. - ## Rebasing invariants @@ -172,7 +169,7 @@ There is inevitable rounding error when rebasing, since there is no possible way > After a non-reverting call to changeSupply(), the new totalSupply should always match what was passed into the call. -> Only transfers change the balance of `StdNonRebasing` and `YieldDelegationSource` accounts. +> Only transfers, mints, and burns change the balance of `StdNonRebasing` and `YieldDelegationSource` accounts. ## Other invariants From 77d69f13f1d84a415570554f7d2d9189c08293ce Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Fri, 15 Nov 2024 15:15:50 +0100 Subject: [PATCH 037/110] Transfer unit tests for new token implementation (#2310) * remove nonreentrant modifiers from the toke code as they are not needed * add setup for all account types * check all relevant contract initial states * add test between any possible contract accounts * prettier * add some documentation and prettier --- contracts/test/token/ousd.js | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/contracts/test/token/ousd.js b/contracts/test/token/ousd.js index 996af5f481..941e9b439d 100644 --- a/contracts/test/token/ousd.js +++ b/contracts/test/token/ousd.js @@ -1,5 +1,5 @@ const { expect } = require("chai"); -const { loadDefaultFixture } = require("../_fixture"); +const { loadDefaultFixture, loadTokenTransferFixture } = require("../_fixture"); const { utils } = require("ethers"); const { daiUnits, ousdUnits, usdcUnits, isFork } = require("../helpers"); @@ -892,4 +892,22 @@ describe("Token", function () { //A Should delegate to account B, rebase with profit, delegate to account C and all have correct balances }); }); + + describe("Old code migrated contract accounts", function () { + beforeEach(async () => { + fixture = await loadTokenTransferFixture(); + }); + + it("Old code auto migrated contract when calling rebase OptIn shouldn't affect invariables", async () => { + const { + nonrebase_cotract_notSet_altcpt_gt_0: contract_account, + ousd + } = fixture; + + const nonRebasingSupply = await ousd.nonRebasingSupply(); + await contract_account.rebaseOptIn(); + + await expect(nonRebasingSupply).to.equal(await ousd.nonRebasingSupply()); + }); + }); }); From 0cfa35276beee1fa0da43c31fb05a36bf178594e Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Fri, 15 Nov 2024 15:50:34 +0100 Subject: [PATCH 038/110] fix test --- contracts/test/token/ousd.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/test/token/ousd.js b/contracts/test/token/ousd.js index 941e9b439d..00c893c365 100644 --- a/contracts/test/token/ousd.js +++ b/contracts/test/token/ousd.js @@ -899,15 +899,15 @@ describe("Token", function () { }); it("Old code auto migrated contract when calling rebase OptIn shouldn't affect invariables", async () => { - const { - nonrebase_cotract_notSet_altcpt_gt_0: contract_account, - ousd - } = fixture; + const { nonrebase_cotract_notSet_altcpt_gt_0: contract_account, ousd } = + fixture; const nonRebasingSupply = await ousd.nonRebasingSupply(); await contract_account.rebaseOptIn(); - await expect(nonRebasingSupply).to.equal(await ousd.nonRebasingSupply()); + await expect( + nonRebasingSupply.sub(await ousd.balanceOf(contract_account.address)) + ).to.equal(await ousd.nonRebasingSupply()); }); }); }); From 31c80785df784044ed9d8105018e3dd24ddbb892 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Fri, 15 Nov 2024 17:08:20 +0100 Subject: [PATCH 039/110] remove redundant state checks --- contracts/contracts/token/OUSD.sol | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index dead3a4f93..4985837e55 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -650,18 +650,10 @@ contract OUSD is Governable { "Invalid rebaseState to" ); - if ( - alternativeCreditsPerToken[from] == 0 && - (stateFrom == RebaseOptions.NotSet || - stateFrom == RebaseOptions.StdRebasing) - ) { + if (alternativeCreditsPerToken[from] == 0) { _rebaseOptOut(from); } - if ( - alternativeCreditsPerToken[to] > 0 && - (stateTo == RebaseOptions.NotSet || - stateTo == RebaseOptions.StdNonRebasing) - ) { + if (alternativeCreditsPerToken[to] > 0) { _rebaseOptIn(to); } From 511dcaa68949f95e07f902939d0295c293812d08 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Fri, 15 Nov 2024 17:45:45 +0100 Subject: [PATCH 040/110] remove overwriting the same value to alternativeCreditsPerToken --- contracts/contracts/token/OUSD.sol | 3 --- 1 file changed, 3 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 4985837e55..b586fa2a18 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -296,7 +296,6 @@ contract OUSD is Governable { _creditBalances[account] = newBalance; _creditBalances[target] = targetNewCredits; - alternativeCreditsPerToken[account] = 1e18; } else if (state == RebaseOptions.YieldDelegationTarget) { uint256 newCredits = _balanceToRebasingCredits( newBalance + _creditBalances[yieldFrom[account]] @@ -694,9 +693,7 @@ contract OUSD is Governable { // Local _creditBalances[from] = fromBalance; - alternativeCreditsPerToken[from] = 1e18; _creditBalances[to] = toNewCredits; - alternativeCreditsPerToken[to] = 0; // Is needed otherwise rebaseOptOut check will not pass // Global nonRebasingSupply += fromBalance; From 5f63650cfe5d629cd35445d097fdf4cda4d25017 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Fri, 15 Nov 2024 17:50:56 +0100 Subject: [PATCH 041/110] add non zero checks in delegation functions --- contracts/contracts/token/OUSD.sol | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index b586fa2a18..f209aed67b 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -624,6 +624,9 @@ contract OUSD is Governable { } function delegateYield(address from, address to) external onlyGovernor { + require(yieldTo[from] != address(0), "Zero from address not allowed"); + require(yieldTo[to] != address(0), "Zero to address not allowed"); + require(from != to, "Cannot delegate to self"); require( yieldFrom[to] == address(0) && @@ -677,7 +680,7 @@ contract OUSD is Governable { function undelegateYield(address from) external onlyGovernor { // Require a delegation, which will also ensure a valid delegation - require(yieldTo[from] != address(0), ""); + require(yieldTo[from] != address(0), "Zero address not allowed"); address to = yieldTo[from]; uint256 fromBalance = balanceOf(from); From 4e327faa095e83c89a9c61f48921c1c751e1d457 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Fri, 15 Nov 2024 18:01:07 +0100 Subject: [PATCH 042/110] correct error --- contracts/contracts/token/OUSD.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index f209aed67b..9e772c3aad 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -624,8 +624,8 @@ contract OUSD is Governable { } function delegateYield(address from, address to) external onlyGovernor { - require(yieldTo[from] != address(0), "Zero from address not allowed"); - require(yieldTo[to] != address(0), "Zero to address not allowed"); + require(from != address(0), "Zero from address not allowed"); + require(to != address(0), "Zero to address not allowed"); require(from != to, "Cannot delegate to self"); require( @@ -689,7 +689,7 @@ contract OUSD is Governable { uint256 toNewCredits = _balanceToRebasingCredits(toBalance); // Remove the bidirectional links - yieldFrom[yieldTo[from]] = address(0); + yieldFrom[to] = address(0); yieldTo[from] = address(0); rebaseState[from] = RebaseOptions.StdNonRebasing; rebaseState[to] = RebaseOptions.StdRebasing; From 9e9c72601cc70aa38c916247f87da1ab2e9d8db0 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Fri, 15 Nov 2024 18:17:07 +0100 Subject: [PATCH 043/110] correct some contract view modifiers --- contracts/contracts/token/OUSD.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 9e772c3aad..db6300eac8 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -141,6 +141,7 @@ contract OUSD is Governable { uint256 baseBalance = (_creditBalances[_account] * 1e18) / _creditsPerToken(_account); if (state == RebaseOptions.YieldDelegationTarget) { + // _creditBalances of yieldFrom accounts equals token balances return baseBalance - _creditBalances[yieldFrom[_account]]; } return baseBalance; @@ -154,7 +155,7 @@ contract OUSD is Governable { * address */ function creditsBalanceOf(address _account) - public + external view returns (uint256, uint256) { @@ -179,7 +180,7 @@ contract OUSD is Governable { * address, and isUpgraded */ function creditsBalanceOfHighres(address _account) - public + external view returns ( uint256, From 6e1b63e4b3aeb11f694018199b5f55f2dd41e8dd Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Fri, 15 Nov 2024 18:17:43 +0100 Subject: [PATCH 044/110] add a readable error message when allowance is exceeded --- contracts/contracts/token/OUSD.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index db6300eac8..f29b010773 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -232,6 +232,7 @@ contract OUSD is Governable { uint256 _value ) external returns (bool) { require(_to != address(0), "Transfer to zero address"); + require(_value <= _allowances[_from][msg.sender], "Allowance exceeded"); _allowances[_from][msg.sender] = _allowances[_from][msg.sender] - From 659f294ab42b8e03746bcb2ed4d362d9ebe00021 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Fri, 15 Nov 2024 18:42:13 +0100 Subject: [PATCH 045/110] reducing 2 functions to 1 --- contracts/contracts/token/OUSD.sol | 18 ++---------------- contracts/test/token/ousd.js | 2 +- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index f29b010773..2c26a0dc3c 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -369,13 +369,6 @@ contract OUSD is Governable { return true; } - /** - * @dev Mints new tokens, increasing totalSupply. - */ - function mint(address _account, uint256 _amount) external onlyVault { - _mint(_account, _amount); - } - /** * @dev Creates `_amount` tokens and assigns them to `_account`, increasing * the total supply. @@ -386,7 +379,7 @@ contract OUSD is Governable { * * - `to` cannot be the zero address. */ - function _mint(address _account, uint256 _amount) internal { + function mint(address _account, uint256 _amount) external onlyVault { require(_account != address(0), "Mint to the zero address"); // Account @@ -402,13 +395,6 @@ contract OUSD is Governable { emit Transfer(address(0), _account, _amount); } - /** - * @dev Burns tokens, decreasing totalSupply. - */ - function burn(address account, uint256 amount) external onlyVault { - _burn(account, amount); - } - /** * @dev Destroys `_amount` tokens from `_account`, reducing the * total supply. @@ -420,7 +406,7 @@ contract OUSD is Governable { * - `_account` cannot be the zero address. * - `_account` must have at least `_amount` tokens. */ - function _burn(address _account, uint256 _amount) internal { + function burn(address _account, uint256 _amount) external onlyVault { require(_account != address(0), "Burn from the zero address"); if (_amount == 0) { return; diff --git a/contracts/test/token/ousd.js b/contracts/test/token/ousd.js index 00c893c365..79520fa756 100644 --- a/contracts/test/token/ousd.js +++ b/contracts/test/token/ousd.js @@ -616,7 +616,7 @@ describe("Token", function () { await anna.getAddress(), ousdUnits("100") ) - ).to.be.revertedWith("panic code 0x11"); + ).to.be.revertedWith("Allowance exceeded"); }); it("Should increase users balance on supply increase", async () => { From f2617565a7dae3dc765ca7c23209b1a08729ea94 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 18 Nov 2024 11:00:01 +0100 Subject: [PATCH 046/110] deprecate isUpgraded --- contracts/contracts/token/OUSD.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 2c26a0dc3c..db7e0a29d1 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -54,7 +54,7 @@ contract OUSD is Governable { uint256 public nonRebasingSupply; // All nonrebasing balances mapping(address => uint256) internal alternativeCreditsPerToken; mapping(address => RebaseOptions) public rebaseState; - mapping(address => uint256) public isUpgraded; + mapping(address => uint256) private __deprecated_isUpgraded; mapping(address => address) public yieldTo; mapping(address => address) public yieldFrom; @@ -191,7 +191,7 @@ contract OUSD is Governable { return ( _creditBalances[_account], _creditsPerToken(_account), - isUpgraded[_account] == 1 + true // all accounts have their resolution "upgraded" ); } From 54b7ebe79eab78db8b866fdb76b827aa07e0b500 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 18 Nov 2024 12:28:43 +0100 Subject: [PATCH 047/110] rename totalSupply --- contracts/contracts/token/OUSD.sol | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index db7e0a29d1..844a9a472f 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -45,7 +45,7 @@ contract OUSD is Governable { // Add slots to align with deployed OUSD contract uint256[154] private _gap; uint256 private constant MAX_SUPPLY = ~uint128(0); // (2^128) - 1 - uint256 public _totalSupply; + uint256 public totalSupply; mapping(address => mapping(address => uint256)) private _allowances; address public vaultAddress = address(0); mapping(address => uint256) internal _creditBalances; @@ -90,13 +90,6 @@ contract OUSD is Governable { _; } - /** - * @return The total supply of OUSD. - */ - function totalSupply() external view returns (uint256) { - return _totalSupply; - } - /** * @return High resolution rebasingCreditsPerToken */ @@ -389,9 +382,9 @@ contract OUSD is Governable { ) = _adjustAccount(_account, _amount.toInt256()); // Globals _adjustGlobals(toRebasingCreditsDiff, toNonRebasingSupplyDiff); - _totalSupply = _totalSupply + _amount; + totalSupply = totalSupply + _amount; - require(_totalSupply < MAX_SUPPLY, "Max supply"); + require(totalSupply < MAX_SUPPLY, "Max supply"); emit Transfer(address(0), _account, _amount); } @@ -419,7 +412,7 @@ contract OUSD is Governable { ) = _adjustAccount(_account, -_amount.toInt256()); // Globals _adjustGlobals(toRebasingCreditsDiff, toNonRebasingSupplyDiff); - _totalSupply = _totalSupply - _amount; + totalSupply = totalSupply - _amount; emit Transfer(_account, address(0), _amount); } @@ -579,33 +572,33 @@ contract OUSD is Governable { * @param _newTotalSupply New total supply of OUSD. */ function changeSupply(uint256 _newTotalSupply) external onlyVault { - require(_totalSupply > 0, "Cannot increase 0 supply"); + require(totalSupply > 0, "Cannot increase 0 supply"); - if (_totalSupply == _newTotalSupply) { + if (totalSupply == _newTotalSupply) { emit TotalSupplyUpdatedHighres( - _totalSupply, + totalSupply, _rebasingCredits, _rebasingCreditsPerToken ); return; } - _totalSupply = _newTotalSupply > MAX_SUPPLY + totalSupply = _newTotalSupply > MAX_SUPPLY ? MAX_SUPPLY : _newTotalSupply; _rebasingCreditsPerToken = (_rebasingCredits * 1e18) / - (_totalSupply - nonRebasingSupply); + (totalSupply - nonRebasingSupply); require(_rebasingCreditsPerToken > 0, "Invalid change in supply"); - _totalSupply = + totalSupply = ((_rebasingCredits * 1e18) / _rebasingCreditsPerToken) + nonRebasingSupply; emit TotalSupplyUpdatedHighres( - _totalSupply, + totalSupply, _rebasingCredits, _rebasingCreditsPerToken ); From 2b31fd654bdb9fbd4536badbbb1c7e78433f59a8 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 18 Nov 2024 12:32:46 +0100 Subject: [PATCH 048/110] improve initialisation checks --- contracts/contracts/token/OUSD.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 844a9a472f..f1867cb1df 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -64,8 +64,9 @@ contract OUSD is Governable { external onlyGovernor { + require(_vaultAddress != address(0), "Zero vault address"); require(vaultAddress == address(0), "Already initialized"); - require(_rebasingCreditsPerToken == 0, "Already initialized"); + _rebasingCreditsPerToken = _initialCreditsPerToken; vaultAddress = _vaultAddress; } From 108f0ebac7abb80d0c7dadd012b4c389d86fd1cc Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 18 Nov 2024 12:36:43 +0100 Subject: [PATCH 049/110] remove gas optimisation that would also allow for errorneously large transfers --- contracts/contracts/token/OUSD.sol | 4 ---- 1 file changed, 4 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index f1867cb1df..74e1d362ae 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -250,10 +250,6 @@ contract OUSD is Governable { address _to, uint256 _value ) internal { - if (_from == _to) { - return; - } - ( int256 fromRebasingCreditsDiff, int256 fromNonRebasingSupplyDiff From 24865d02a9f0e29116f72b71490eed0dbee375df Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 18 Nov 2024 12:38:28 +0100 Subject: [PATCH 050/110] remove underflow checks --- contracts/contracts/token/OUSD.sol | 6 ------ 1 file changed, 6 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 74e1d362ae..aeae1cbf2a 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -317,16 +317,10 @@ contract OUSD is Governable { int256 nonRebasingSupplyDiff ) internal { if (rebasingCreditsDiff != 0) { - if (_rebasingCredits.toInt256() + rebasingCreditsDiff < 0) { - revert("rebasingCredits underflow"); - } _rebasingCredits = (_rebasingCredits.toInt256() + rebasingCreditsDiff).toUint256(); } if (nonRebasingSupplyDiff != 0) { - if (nonRebasingSupply.toInt256() + nonRebasingSupplyDiff < 0) { - revert("nonRebasingSupply underflow"); - } nonRebasingSupply = (nonRebasingSupply.toInt256() + nonRebasingSupplyDiff).toUint256(); } From b728d2e96289b5dbc1f1da2ba85feb25420579e9 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 18 Nov 2024 12:56:08 +0100 Subject: [PATCH 051/110] simplify code check for rebaseOptIn and add a test for it --- contracts/contracts/token/OUSD.sol | 9 +++++---- contracts/test/token/ousd.js | 17 +++++++++++++++-- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index aeae1cbf2a..25fd359c82 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -498,12 +498,13 @@ contract OUSD is Governable { function _rebaseOptIn(address _account) internal { // prettier-ignore require( - alternativeCreditsPerToken[_account] > 0 || ( - _account.code.length > 0 && // isContract + alternativeCreditsPerToken[_account] > 0 || + // new empty contracts that haven't been yet autoMigrated to StdNonRebasing can + // explicitly call `rebaseOptIn`. Side effect is that also already rebasing EOA + // accounts that have 0 balance can call RebaseOptIn balanceOf(_account) == 0 - ) , - "Account must be non-rebasing or empty contract" + "Account must be non-rebasing" ); RebaseOptions state = rebaseState[_account]; // prettier-ignore diff --git a/contracts/test/token/ousd.js b/contracts/test/token/ousd.js index 79520fa756..c89e4aa71a 100644 --- a/contracts/test/token/ousd.js +++ b/contracts/test/token/ousd.js @@ -490,12 +490,25 @@ describe("Token", function () { }); it("Should not allow EOA to call rebaseOptIn when already opted in to rebasing", async () => { - let { ousd, matt } = fixture; + let { ousd, matt, usdc } = fixture; + await usdc.connect(matt).mint(usdcUnits("2")); + await expect(ousd.connect(matt).rebaseOptIn()).to.be.revertedWith( - "Account must be non-rebasing or empty contract" + "Account must be non-rebasing" ); }); + it("Should allow an EOA to call rebaseOptIn when already opted in to rebasing", async () => { + let { ousd, matt, usdc, josh } = fixture; + await usdc.connect(matt).mint(usdcUnits("2")); + // transfer all OUSD out + await ousd.connect(matt).transfer(josh.address, await ousd.balanceOf(matt.address)); + + // user is allowed to override its NotSet rebasing state to Rebasing without negatively affecting + // any of the token contract's invariants + await ousd.connect(matt).rebaseOptIn(); + }); + it("Should not allow EOA to call rebaseOptOut when already opted out of rebasing", async () => { let { ousd, matt } = fixture; await ousd.connect(matt).rebaseOptOut(); From ba625f09bd6ed486f307d5f716c7090843e09ac6 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 18 Nov 2024 23:42:22 +0100 Subject: [PATCH 052/110] remove comments --- contracts/test/_fixture.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index 3f06329f9c..04a6a94903 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -253,9 +253,6 @@ const createAccountTypes = async ({ vault, ousd, ousdUnlocked, deploy }) => { await vault.connect(matt).mint(dai.address, daiUnits("1000"), 0); } - // yiedlsource - // yieldtarget - const createAccount = async () => { let account = ethers.Wallet.createRandom(); // Give ETH to user From f8280d13e2f5411f1da0c636f22f333e1cbcce06 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 18 Nov 2024 23:49:54 +0100 Subject: [PATCH 053/110] move the balance and credits query above the rebaseState changes --- contracts/contracts/token/OUSD.sol | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 25fd359c82..65e1b75ad2 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -632,23 +632,23 @@ contract OUSD is Governable { _rebaseOptIn(to); } + uint256 balanceFrom = balanceOf(from); + uint256 creditsFrom = _balanceToRebasingCredits(balanceFrom); + // Set up the bidirectional links yieldTo[from] = to; yieldFrom[to] = from; rebaseState[from] = RebaseOptions.YieldDelegationSource; rebaseState[to] = RebaseOptions.YieldDelegationTarget; - uint256 balance = balanceOf(from); - uint256 credits = _balanceToRebasingCredits(balance); - // Local - _creditBalances[from] = balance; + _creditBalances[from] = balanceFrom; alternativeCreditsPerToken[from] = 1e18; - _creditBalances[to] += credits; + _creditBalances[to] += creditsFrom; // Global - nonRebasingSupply -= balance; - _rebasingCredits += credits; + nonRebasingSupply -= balanceFrom; + _rebasingCredits += creditsFrom; } function undelegateYield(address from) external onlyGovernor { From ab460b8f6353435dbc3a49a7285082818f2ee98e Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 18 Nov 2024 23:58:49 +0100 Subject: [PATCH 054/110] use _adjustGlobals function to adjust globals in yield delegation --- contracts/contracts/token/OUSD.sol | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 65e1b75ad2..c8f8cd0288 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -632,8 +632,8 @@ contract OUSD is Governable { _rebaseOptIn(to); } - uint256 balanceFrom = balanceOf(from); - uint256 creditsFrom = _balanceToRebasingCredits(balanceFrom); + uint256 fromBalance = balanceOf(from); + uint256 creditsFrom = _balanceToRebasingCredits(fromBalance); // Set up the bidirectional links yieldTo[from] = to; @@ -642,13 +642,11 @@ contract OUSD is Governable { rebaseState[to] = RebaseOptions.YieldDelegationTarget; // Local - _creditBalances[from] = balanceFrom; + _creditBalances[from] = fromBalance; alternativeCreditsPerToken[from] = 1e18; _creditBalances[to] += creditsFrom; - // Global - nonRebasingSupply -= balanceFrom; - _rebasingCredits += creditsFrom; + _adjustGlobals(creditsFrom.toInt256(), -fromBalance.toInt256()); } function undelegateYield(address from) external onlyGovernor { @@ -671,8 +669,6 @@ contract OUSD is Governable { _creditBalances[from] = fromBalance; _creditBalances[to] = toNewCredits; - // Global - nonRebasingSupply += fromBalance; - _rebasingCredits -= (toCreditsBefore - toNewCredits); // Should always go down or stay the same + _adjustGlobals(-(toCreditsBefore - toNewCredits).toInt256(), fromBalance.toInt256()); } } From c94cd5fe66f70b9d38565e4d1509c12ff599a9d2 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 19 Nov 2024 00:08:58 +0100 Subject: [PATCH 055/110] futher simplify the undelegateYield function --- contracts/contracts/token/OUSD.sol | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index c8f8cd0288..568e30771c 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -655,9 +655,8 @@ contract OUSD is Governable { address to = yieldTo[from]; uint256 fromBalance = balanceOf(from); - uint256 toBalance = balanceOf(to); - uint256 toCreditsBefore = _creditBalances[to]; - uint256 toNewCredits = _balanceToRebasingCredits(toBalance); + // these are credits of from account if it were rebasing + uint256 fromCredits = _balanceToRebasingCredits(fromBalance); // Remove the bidirectional links yieldFrom[to] = address(0); @@ -667,8 +666,8 @@ contract OUSD is Governable { // Local _creditBalances[from] = fromBalance; - _creditBalances[to] = toNewCredits; + _creditBalances[to] -= fromCredits; - _adjustGlobals(-(toCreditsBefore - toNewCredits).toInt256(), fromBalance.toInt256()); + _adjustGlobals(-(fromCredits).toInt256(), fromBalance.toInt256()); } } From c3684a5ed5dd4d7eca8649f6f678417be23d3821 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 19 Nov 2024 00:10:32 +0100 Subject: [PATCH 056/110] unify the variable names --- contracts/contracts/token/OUSD.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 568e30771c..8e02962147 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -656,7 +656,7 @@ contract OUSD is Governable { address to = yieldTo[from]; uint256 fromBalance = balanceOf(from); // these are credits of from account if it were rebasing - uint256 fromCredits = _balanceToRebasingCredits(fromBalance); + uint256 creditsFrom = _balanceToRebasingCredits(fromBalance); // Remove the bidirectional links yieldFrom[to] = address(0); @@ -666,8 +666,8 @@ contract OUSD is Governable { // Local _creditBalances[from] = fromBalance; - _creditBalances[to] -= fromCredits; + _creditBalances[to] -= creditsFrom; - _adjustGlobals(-(fromCredits).toInt256(), fromBalance.toInt256()); + _adjustGlobals(-(creditsFrom).toInt256(), fromBalance.toInt256()); } } From 9cc74cc7d492f6e82ed05d52eee1c72734658c3a Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 19 Nov 2024 00:14:04 +0100 Subject: [PATCH 057/110] add comment --- contracts/contracts/token/OUSD.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 8e02962147..56ea03875c 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -666,6 +666,8 @@ contract OUSD is Governable { // Local _creditBalances[from] = fromBalance; + // no need to set the alternativeCreditsPerToken of the from account since + // that one is already set to 1e18 by the `delegateYield` function. _creditBalances[to] -= creditsFrom; _adjustGlobals(-(creditsFrom).toInt256(), fromBalance.toInt256()); From 2987362afe29363506fe7d515ed32f425256f1e9 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 19 Nov 2024 00:39:53 +0100 Subject: [PATCH 058/110] a couple of more places to utilise the _adjustGlobals function --- contracts/contracts/token/OUSD.sol | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 56ea03875c..6f3699c713 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -521,9 +521,7 @@ contract OUSD is Governable { alternativeCreditsPerToken[msg.sender] = 0; _creditBalances[msg.sender] = _balanceToRebasingCredits(balance); - // Globals - nonRebasingSupply -= balance; - _rebasingCredits += _creditBalances[msg.sender]; + _adjustGlobals(_creditBalances[msg.sender].toInt256(), -balance.toInt256()); emit AccountRebasingEnabled(_account); } @@ -551,9 +549,7 @@ contract OUSD is Governable { alternativeCreditsPerToken[_account] = 1e18; _creditBalances[_account] = balance; - // Globals - nonRebasingSupply += balance; - _rebasingCredits -= oldCredits; + _adjustGlobals(-oldCredits.toInt256(), balance.toInt256()); emit AccountRebasingDisabled(_account); } From 6b023877134b7f374ea83db68d3263768b9ed2fb Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 19 Nov 2024 00:42:01 +0100 Subject: [PATCH 059/110] no need this being a separate variable --- contracts/contracts/token/OUSD.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 6f3699c713..8f5876239b 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -541,7 +541,6 @@ contract OUSD is Governable { "Only standard rebasing accounts can opt out" ); - uint256 oldCredits = _creditBalances[_account]; uint256 balance = balanceOf(_account); // Account @@ -549,7 +548,7 @@ contract OUSD is Governable { alternativeCreditsPerToken[_account] = 1e18; _creditBalances[_account] = balance; - _adjustGlobals(-oldCredits.toInt256(), balance.toInt256()); + _adjustGlobals(-_creditBalances[_account].toInt256(), balance.toInt256()); emit AccountRebasingDisabled(_account); } From 6556835a4f940aff81b4c47ad5f8d9c891b76b00 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 19 Nov 2024 00:52:28 +0100 Subject: [PATCH 060/110] undo bug introduction --- contracts/contracts/token/OUSD.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 8f5876239b..6f3699c713 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -541,6 +541,7 @@ contract OUSD is Governable { "Only standard rebasing accounts can opt out" ); + uint256 oldCredits = _creditBalances[_account]; uint256 balance = balanceOf(_account); // Account @@ -548,7 +549,7 @@ contract OUSD is Governable { alternativeCreditsPerToken[_account] = 1e18; _creditBalances[_account] = balance; - _adjustGlobals(-_creditBalances[_account].toInt256(), balance.toInt256()); + _adjustGlobals(-oldCredits.toInt256(), balance.toInt256()); emit AccountRebasingDisabled(_account); } From 56b11e29afc89ba0af43f80f20ebcb96e2a6b282 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 19 Nov 2024 00:53:39 +0100 Subject: [PATCH 061/110] wrong use of msg.sender bug fix --- contracts/contracts/token/OUSD.sol | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 6f3699c713..0220a2c9c2 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -514,14 +514,14 @@ contract OUSD is Governable { "Only standard non-rebasing accounts can opt in" ); - uint256 balance = balanceOf(msg.sender); + uint256 balance = balanceOf(_account); // Account - rebaseState[msg.sender] = RebaseOptions.StdRebasing; - alternativeCreditsPerToken[msg.sender] = 0; - _creditBalances[msg.sender] = _balanceToRebasingCredits(balance); + rebaseState[_account] = RebaseOptions.StdRebasing; + alternativeCreditsPerToken[_account] = 0; + _creditBalances[_account] = _balanceToRebasingCredits(balance); - _adjustGlobals(_creditBalances[msg.sender].toInt256(), -balance.toInt256()); + _adjustGlobals(_creditBalances[_account].toInt256(), -balance.toInt256()); emit AccountRebasingEnabled(_account); } From bbb3f45defdcb7b52460fb06badb07620b4ecdd8 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 19 Nov 2024 14:12:10 +0100 Subject: [PATCH 062/110] function doesn't need to return any values --- contracts/contracts/token/OUSD.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 0220a2c9c2..537194721f 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -445,7 +445,7 @@ contract OUSD is Governable { * * @param _account Address of the account. */ - function _autoMigrate(address _account) internal returns (bool) { + function _autoMigrate(address _account) internal { bool isContract = _account.code.length > 0; // In the older contract implementation: https://github.com/OriginProtocol/origin-dollar/blob/20a21d00a4a6ea9f42940ac194e82655fcda882e/contracts/contracts/token/OUSD.sol#L479-L489 // an account could have non 0 balance, be (or become) a contract with the rebase state From e949129d0669caebfdbaa72109c34adc0c888732 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 19 Nov 2024 14:13:14 +0100 Subject: [PATCH 063/110] simplify --- contracts/contracts/token/OUSD.sol | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 537194721f..dcd46ccd93 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -228,9 +228,7 @@ contract OUSD is Governable { require(_to != address(0), "Transfer to zero address"); require(_value <= _allowances[_from][msg.sender], "Allowance exceeded"); - _allowances[_from][msg.sender] = - _allowances[_from][msg.sender] - - _value; + _allowances[_from][msg.sender] -= _value; _executeTransfer(_from, _to, _value); From a1d3cdc27789729e3df9fbd6c7d195e2594643d9 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 19 Nov 2024 14:33:13 +0100 Subject: [PATCH 064/110] improve syntax --- contracts/contracts/token/OUSD.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index dcd46ccd93..62c7c87c83 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -44,7 +44,7 @@ contract OUSD is Governable { // Add slots to align with deployed OUSD contract uint256[154] private _gap; - uint256 private constant MAX_SUPPLY = ~uint128(0); // (2^128) - 1 + uint256 private constant MAX_SUPPLY = type(uint128).max; uint256 public totalSupply; mapping(address => mapping(address => uint256)) private _allowances; address public vaultAddress = address(0); From 504a4212b69f268ab0e05ef2b28c558b7e81e156 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 19 Nov 2024 14:33:38 +0100 Subject: [PATCH 065/110] add events for yield delegation --- contracts/contracts/token/OUSD.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 62c7c87c83..8792fc13bc 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -33,6 +33,8 @@ contract OUSD is Governable { address indexed spender, uint256 value ); + event YieldDelegated(address source, address target); + event YieldUndelegated(address source, address target); enum RebaseOptions { NotSet, @@ -641,6 +643,7 @@ contract OUSD is Governable { _creditBalances[to] += creditsFrom; _adjustGlobals(creditsFrom.toInt256(), -fromBalance.toInt256()); + emit YieldDelegated(from, to); } function undelegateYield(address from) external onlyGovernor { @@ -665,5 +668,6 @@ contract OUSD is Governable { _creditBalances[to] -= creditsFrom; _adjustGlobals(-(creditsFrom).toInt256(), fromBalance.toInt256()); + emit YieldUndelegated(from, to); } } From 546695ff67a06e40bd71f2e6588a44672b2747c4 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 19 Nov 2024 14:38:26 +0100 Subject: [PATCH 066/110] remove var init --- contracts/contracts/token/OUSD.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 8792fc13bc..29783aa061 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -49,7 +49,7 @@ contract OUSD is Governable { uint256 private constant MAX_SUPPLY = type(uint128).max; uint256 public totalSupply; mapping(address => mapping(address => uint256)) private _allowances; - address public vaultAddress = address(0); + address public vaultAddress; mapping(address => uint256) internal _creditBalances; uint256 private _rebasingCredits; // Sum of all rebasing credits (_creditBalances for rebasing accounts) uint256 private _rebasingCreditsPerToken; From 62c1045c9f134cb0c795ae4a840ed0f353e4e657 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 19 Nov 2024 16:29:59 +0100 Subject: [PATCH 067/110] fix deploy file --- contracts/deploy/base/021_upgrade_oeth.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/deploy/base/021_upgrade_oeth.js b/contracts/deploy/base/021_upgrade_oeth.js index 48a24160c6..c2fd99fae3 100644 --- a/contracts/deploy/base/021_upgrade_oeth.js +++ b/contracts/deploy/base/021_upgrade_oeth.js @@ -6,7 +6,7 @@ module.exports = deployOnBaseWithGuardian( deployName: "021_upgrade_oeth", }, async ({ ethers }) => { - const dOETHb = await deployWithConfirmation("OETH", [], "OETH", true); + const dOETHb = await deployWithConfirmation("OETHBase"); const cOETHbProxy = await ethers.getContract("OETHBaseProxy"); From fd45920d5c1e7cf6f600b1da6fc31505855689df Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 20 Nov 2024 10:53:20 +0100 Subject: [PATCH 068/110] add tests to catch possible incorrect rebaseOptIn / rebaseOptOut attributions --- contracts/test/_fixture.js | 7 +- contracts/test/token/ousd.js | 2 - contracts/test/token/token-transfers.js | 146 ++++++++++++++++++++---- 3 files changed, 129 insertions(+), 26 deletions(-) diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index 04a6a94903..72c4841ea2 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -495,12 +495,15 @@ const loadTokenTransferFixture = deployments.createFixture(async () => { const vaultAndTokenConracts = await getVaultAndTokenConracts(); + const signers = await hre.ethers.getSigners(); + let governor = signers[1]; + let strategist = signers[0]; + const accountTypes = await createAccountTypes({ ousd: vaultAndTokenConracts.ousd, ousdUnlocked: vaultAndTokenConracts.ousdUnlocked, vault: vaultAndTokenConracts.vault, deploy: deployments.deploy, - governor: deployments.governor, }); return { @@ -509,6 +512,8 @@ const loadTokenTransferFixture = deployments.createFixture(async () => { governorAddr, strategistAddr, timelockAddr, + governor, + strategist, }; }); diff --git a/contracts/test/token/ousd.js b/contracts/test/token/ousd.js index c89e4aa71a..70e87bf360 100644 --- a/contracts/test/token/ousd.js +++ b/contracts/test/token/ousd.js @@ -842,8 +842,6 @@ describe("Token", function () { await expect(matt).has.an.approxBalanceOf("80.00", ousd); await expect(anna).has.an.approxBalanceOf("10", ousd); - // TODO: delete rebase opt out later - await ousd.connect(matt).rebaseOptOut(); await ousd .connect(governor) // matt delegates yield to anna diff --git a/contracts/test/token/token-transfers.js b/contracts/test/token/token-transfers.js index 2c1dd9bb9f..5c3e0fdc59 100644 --- a/contracts/test/token/token-transfers.js +++ b/contracts/test/token/token-transfers.js @@ -3,7 +3,7 @@ const { loadTokenTransferFixture } = require("../_fixture"); const { isFork, ousdUnits } = require("../helpers"); -describe("Token Transfers", function () { +describe("Account type variations", function () { if (isFork) { this.timeout(0); } @@ -119,61 +119,107 @@ describe("Token Transfers", function () { const fromAccounts = [ { name: "rebase_eoa_notset_0", - affectsRebasingCredits: true, + balancePartOfRebasingCredits: true, isContract: false, + inYieldDelegation: false, }, { name: "rebase_eoa_stdRebasing_0", - affectsRebasingCredits: true, + balancePartOfRebasingCredits: true, isContract: false, + inYieldDelegation: false, }, { name: "rebase_contract_0", - affectsRebasingCredits: true, + balancePartOfRebasingCredits: true, isContract: true, + inYieldDelegation: false, }, { name: "nonrebase_eoa_0", - affectsRebasingCredits: false, + balancePartOfRebasingCredits: false, isContract: false, + inYieldDelegation: false, }, { name: "nonrebase_cotract_0", - affectsRebasingCredits: false, + balancePartOfRebasingCredits: false, isContract: true, + inYieldDelegation: false, + }, + { + name: "nonrebase_cotract_notSet_0", + balancePartOfRebasingCredits: false, + skipTransferTest: true, + isContract: true, + inYieldDelegation: false, }, - // can not initiate a transfer from below contract since it has the balance of 0 - //{name: "nonrebase_cotract_notSet_0", affectsRebasingCredits: false, isContract: true}, { name: "nonrebase_cotract_notSet_altcpt_gt_0", - affectsRebasingCredits: false, + balancePartOfRebasingCredits: false, isContract: true, + inYieldDelegation: false, }, { name: "rebase_delegate_source_0", - affectsRebasingCredits: true, + balancePartOfRebasingCredits: true, isContract: false, + inYieldDelegation: true, }, { name: "rebase_delegate_target_0", - affectsRebasingCredits: true, + balancePartOfRebasingCredits: true, isContract: false, + inYieldDelegation: true, }, ]; const toAccounts = [ - { name: "rebase_eoa_notset_1", affectsRebasingCredits: true }, - { name: "rebase_eoa_stdRebasing_1", affectsRebasingCredits: true }, - { name: "rebase_contract_1", affectsRebasingCredits: true }, - { name: "nonrebase_eoa_1", affectsRebasingCredits: false }, - { name: "nonrebase_cotract_1", affectsRebasingCredits: false }, - { name: "nonrebase_cotract_notSet_1", affectsRebasingCredits: false }, + { + name: "rebase_eoa_notset_1", + balancePartOfRebasingCredits: true, + inYieldDelegation: false, + }, + { + name: "rebase_eoa_stdRebasing_1", + balancePartOfRebasingCredits: true, + inYieldDelegation: false, + }, + { + name: "rebase_contract_1", + balancePartOfRebasingCredits: true, + inYieldDelegation: false, + }, + { + name: "nonrebase_eoa_1", + balancePartOfRebasingCredits: false, + inYieldDelegation: false, + }, + { + name: "nonrebase_cotract_1", + balancePartOfRebasingCredits: false, + inYieldDelegation: false, + }, + { + name: "nonrebase_cotract_notSet_1", + balancePartOfRebasingCredits: false, + inYieldDelegation: false, + }, { name: "nonrebase_cotract_notSet_altcpt_gt_1", - affectsRebasingCredits: false, + balancePartOfRebasingCredits: false, + inYieldDelegation: false, + }, + { + name: "rebase_delegate_source_1", + balancePartOfRebasingCredits: true, + inYieldDelegation: true, + }, + { + name: "rebase_delegate_target_1", + balancePartOfRebasingCredits: true, + inYieldDelegation: true, }, - { name: "rebase_delegate_source_1", affectsRebasingCredits: true }, - { name: "rebase_delegate_target_1", affectsRebasingCredits: true }, ]; const totalSupply = ousdUnits("792"); @@ -182,13 +228,14 @@ describe("Token Transfers", function () { for (let j = 0; j < toAccounts.length; j++) { const { name: fromName, - affectsRebasingCredits: fromAffectsRC, + balancePartOfRebasingCredits: fromAffectsRC, + skipTransferTest, isContract, } = fromAccounts[i]; - const { name: toName, affectsRebasingCredits: toAffectsRC } = + const { name: toName, balancePartOfRebasingCredits: toAffectsRC } = toAccounts[j]; - it(`Should transfer from ${fromName} to ${toName}`, async () => { + (skipTransferTest ? it.skip : it)(`Should transfer from ${fromName} to ${toName}`, async () => { const fromAccount = fixture[fromName]; const toAccount = fixture[toName]; const { ousd } = fixture; @@ -219,6 +266,59 @@ describe("Token Transfers", function () { if (!toAffectsRC) { expectedNonRebasingSupply = expectedNonRebasingSupply.add(amount); } + + // check global contract (in)variants + await expect(await ousd.totalSupply()).to.equal(totalSupply); + await expect(await ousd.nonRebasingSupply()).to.equal( + expectedNonRebasingSupply + ); + }); + } + } + + for (let i = 0; i < fromAccounts.length; i++) { + for (let j = 0; j < toAccounts.length; j++) { + const { + name: fromName, + balancePartOfRebasingCredits: fromBalancePartOfRC, + inYieldDelegation: inYieldDelegationSource, + } = fromAccounts[i]; + const { + name: toName, + balancePartOfRebasingCredits: toBalancePartOfRC, + inYieldDelegation: inYieldDelegationTarget + } = toAccounts[j]; + + (inYieldDelegationSource || inYieldDelegationTarget ? it.skip : it)(`Non rebasing supply should be correct when ${fromName} delegates to ${toName}`, async () => { + const fromAccount = fixture[fromName]; + const toAccount = fixture[toName]; + const { ousd, governor } = fixture; + + const fromBalance = await ousd.balanceOf(fromAccount.address); + const toBalance = await ousd.balanceOf(toAccount.address); + let expectedNonRebasingSupply = nonRebasingSupply; + + await ousd + .connect(governor) + .delegateYield(fromAccount.address, toAccount.address); + + // check balances haven't changed + await expect(await ousd.balanceOf(fromAccount.address)).to.equal( + fromBalance + ); + await expect(await ousd.balanceOf(toAccount.address)).to.equal( + toBalance + ); + + // account was non rebasing and became rebasing + if (!fromBalancePartOfRC) { + expectedNonRebasingSupply = expectedNonRebasingSupply.sub(fromBalance); + } + // account was non rebasing and became rebasing + if (!toBalancePartOfRC) { + expectedNonRebasingSupply = expectedNonRebasingSupply.sub(toBalance); + } + // check global contract (in)variants await expect(await ousd.totalSupply()).to.equal(totalSupply); await expect(await ousd.nonRebasingSupply()).to.equal( From 3d03c7b2287f23fd43dad6003670283a39b95e0f Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 20 Nov 2024 10:58:07 +0100 Subject: [PATCH 069/110] simplify changeSupply code --- contracts/contracts/token/OUSD.sol | 4 ---- 1 file changed, 4 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 29783aa061..73975e3da6 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -581,10 +581,6 @@ contract OUSD is Governable { require(_rebasingCreditsPerToken > 0, "Invalid change in supply"); - totalSupply = - ((_rebasingCredits * 1e18) / _rebasingCreditsPerToken) + - nonRebasingSupply; - emit TotalSupplyUpdatedHighres( totalSupply, _rebasingCredits, From fbb11a95123ed97ae583477bed3e980062050228 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 20 Nov 2024 16:03:44 +0100 Subject: [PATCH 070/110] add storage slot gap --- contracts/contracts/token/OUSD.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 73975e3da6..9338352f68 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -61,6 +61,8 @@ contract OUSD is Governable { mapping(address => address) public yieldFrom; uint256 private constant RESOLUTION_INCREASE = 1e9; + // including below gap totals up to 200 + uint256[38] private __gap; function initialize(address _vaultAddress, uint256 _initialCreditsPerToken) external From 6d5c745ec6829a20d22f3176534f24239f8cac0b Mon Sep 17 00:00:00 2001 From: Daniel Von Fange Date: Wed, 20 Nov 2024 10:48:59 -0500 Subject: [PATCH 071/110] Comments update --- contracts/contracts/token/OUSD.sol | 130 ++++++++++------------------- 1 file changed, 44 insertions(+), 86 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 9338352f68..72135ee750 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -10,12 +10,6 @@ pragma solidity ^0.8.0; import { Governable } from "../governance/Governable.sol"; import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -/** - * NOTE that this is an ERC20 token but the invariant that the sum of - * balanceOf(x) for all x is not >= totalSupply(). This is a consequence of the - * rebasing design. Any integrations with OUSD should be aware. - */ - contract OUSD is Governable { using SafeCast for int256; using SafeCast for uint256; @@ -44,8 +38,8 @@ contract OUSD is Governable { YieldDelegationTarget } - // Add slots to align with deployed OUSD contract - uint256[154] private _gap; + + uint256[154] private _gap; // Slots to align with deployed contract uint256 private constant MAX_SUPPLY = type(uint128).max; uint256 public totalSupply; mapping(address => mapping(address => uint256)) private _allowances; @@ -61,8 +55,7 @@ contract OUSD is Governable { mapping(address => address) public yieldFrom; uint256 private constant RESOLUTION_INCREASE = 1e9; - // including below gap totals up to 200 - uint256[38] private __gap; + uint256[38] private __gap; // including below gap totals up to 200 function initialize(address _vaultAddress, uint256 _initialCreditsPerToken) external @@ -124,7 +117,7 @@ contract OUSD is Governable { } /** - * @dev Gets the balance of the specified address. + * @notice Gets the balance of the specified address. * @param _account Address to query the balance of. * @return A uint256 representing the amount of base units owned by the * specified address. @@ -146,7 +139,7 @@ contract OUSD is Governable { } /** - * @dev Gets the credits balance of the specified address. + * @notice Gets the credits balance of the specified address. * @dev Backwards compatible with old low res credits per token. * @param _account The address to query the balance of. * @return (uint256, uint256) Credit balance and credits per token of the @@ -172,7 +165,7 @@ contract OUSD is Governable { } /** - * @dev Gets the credits balance of the specified address. + * @notice Gets the credits balance of the specified address. * @param _account The address to query the balance of. * @return (uint256, uint256, bool) Credit balance, credits per token of the * address, and isUpgraded @@ -203,7 +196,7 @@ contract OUSD is Governable { } /** - * @dev Transfer tokens to a specified address. + * @notice Transfer tokens to a specified address. * @param _to the address to transfer to. * @param _value the amount to be transferred. * @return true on success. @@ -214,12 +207,11 @@ contract OUSD is Governable { _executeTransfer(msg.sender, _to, _value); emit Transfer(msg.sender, _to, _value); - return true; } /** - * @dev Transfer tokens from one address to another. + * @notice Transfer tokens from one address to another. * @param _from The address you want to send tokens from. * @param _to The address you want to transfer to. * @param _value The amount of tokens to be transferred. @@ -233,20 +225,12 @@ contract OUSD is Governable { require(_value <= _allowances[_from][msg.sender], "Allowance exceeded"); _allowances[_from][msg.sender] -= _value; - _executeTransfer(_from, _to, _value); emit Transfer(_from, _to, _value); - return true; } - /** - * @dev Update the count of non rebasing credits in response to a transfer - * @param _from The address you want to send tokens from. - * @param _to The address you want to transfer to. - * @param _value Amount of OUSD to transfer - */ function _executeTransfer( address _from, address _to, @@ -329,8 +313,8 @@ contract OUSD is Governable { } /** - * @dev Function to check the amount of tokens that _owner has allowed to - * `_spender`. + * @notice Function to check the amount of tokens that _owner has allowed + * to `_spender`. * @param _owner The address which owns the funds. * @param _spender The address which will spend the funds. * @return The number of tokens still available for the _spender. @@ -344,8 +328,8 @@ contract OUSD is Governable { } /** - * @dev Approve the passed address to spend the specified amount of tokens - * on behalf of msg.sender. + * @notice Approve the passed address to spend the specified amount of + * tokens on behalf of msg.sender. * @param _spender The address which will spend the funds. * @param _value The amount of tokens to be spent. */ @@ -356,14 +340,8 @@ contract OUSD is Governable { } /** - * @dev Creates `_amount` tokens and assigns them to `_account`, increasing - * the total supply. - * - * Emits a {Transfer} event with `from` set to the zero address. - * - * Requirements - * - * - `to` cannot be the zero address. + * @notice Creates `_amount` tokens and assigns them to `_account`, + * increasing the total supply. */ function mint(address _account, uint256 _amount) external onlyVault { require(_account != address(0), "Mint to the zero address"); @@ -382,15 +360,8 @@ contract OUSD is Governable { } /** - * @dev Destroys `_amount` tokens from `_account`, reducing the - * total supply. - * - * Emits a {Transfer} event with `to` set to the zero address. - * - * Requirements - * - * - `_account` cannot be the zero address. - * - `_account` must have at least `_amount` tokens. + * @notice Destroys `_amount` tokens from `_account`, + * reducing the total supply. */ function burn(address _account, uint256 _amount) external onlyVault { require(_account != address(0), "Burn from the zero address"); @@ -428,34 +399,15 @@ contract OUSD is Governable { } /** - * @dev Before a `rebaseOptIn` or non yield delegating token `transfer` can be executed contract - * accounts need to have a more explicitly defined rebasing state set. - * - * Contract account can be in the following states before `autoMigrate` is called: - * 1. Under any token contract codebase they haven't been part of any token transfers yet - * having rebaseState `NotSet` and `alternativeCreditsPerToken == 0` - * 2. Under older token contract codebase they have the default rebaseState set to `NotSet` and - * the codebase has "auto-migrated" them by setting the `alternativeCreditsPerToken` to some - * value greater than 0. - * 3. Contract has under any token contract codebase explicitly requested to be opted out of rebasing - * - * Case 1. Needs to be migrated using autoMigrate to a nonRebasing account. - * - * Note: Even with this _autoMigrate function in place there will still be Case 2 accounts existing that - * will behave exactly like RebaseState StdNonRebasing account, and still having their rebase state - * set to `NotSet` - * + * @dev Auto migrate contracts to be non rebasing, + * unless they have opted into yield. * @param _account Address of the account. */ function _autoMigrate(address _account) internal { bool isContract = _account.code.length > 0; - // In the older contract implementation: https://github.com/OriginProtocol/origin-dollar/blob/20a21d00a4a6ea9f42940ac194e82655fcda882e/contracts/contracts/token/OUSD.sol#L479-L489 - // an account could have non 0 balance, be (or become) a contract with the rebase state - // set to default balanceRebaseOptions.NotSet and alternativeCreditsPerToken > 0. The latter would happen - // when such account would already be once `migrated` by running `_ensureRebasingMigration`. Executing the - // migration for a second time would cause great errors. - // With the current code that is no longer possible since accounts have their rebaseState marked - // as `StdNonRebasing` when running `_rebaseOptOut` + // In previous code versions, contracts would not have had thier + // rebaseState[_account] set to RebaseOptions.NonRebasing when migrated + // therefor we check the actual accounting used on the account instead. if ( isContract && rebaseState[_account] == RebaseOptions.NotSet && @@ -479,9 +431,6 @@ contract OUSD is Governable { /** * @notice Enable rebasing for an account. - * @dev Add a contract address to the non-rebasing exception list. The - * address's balance will be part of rebases and the account will be exposed - * to upside and downside. * @param _account Address of the account. */ function governanceRebaseOptIn(address _account) external onlyGovernor { @@ -489,9 +438,7 @@ contract OUSD is Governable { } /** - * @dev Add a contract address to the non-rebasing exception list. The - * address's balance will be part of rebases and the account will be exposed - * to upside and downside. + * @notice The calling account will start receiving yield after a successful call. */ function rebaseOptIn() external { _rebaseOptIn(msg.sender); @@ -501,9 +448,8 @@ contract OUSD is Governable { // prettier-ignore require( alternativeCreditsPerToken[_account] > 0 || - // new empty contracts that haven't been yet autoMigrated to StdNonRebasing can - // explicitly call `rebaseOptIn`. Side effect is that also already rebasing EOA - // accounts that have 0 balance can call RebaseOptIn + // Accounts may explicitly `rebaseOptIn` regardless of + // accounting if they have a 0 balance. balanceOf(_account) == 0 , "Account must be non-rebasing" @@ -522,12 +468,15 @@ contract OUSD is Governable { rebaseState[_account] = RebaseOptions.StdRebasing; alternativeCreditsPerToken[_account] = 0; _creditBalances[_account] = _balanceToRebasingCredits(balance); - + // Globals _adjustGlobals(_creditBalances[_account].toInt256(), -balance.toInt256()); emit AccountRebasingEnabled(_account); } + /** + * @notice The calling account will no longer receive yield + */ function rebaseOptOut() external { _rebaseOptOut(msg.sender); } @@ -550,15 +499,15 @@ contract OUSD is Governable { rebaseState[_account] = RebaseOptions.StdNonRebasing; alternativeCreditsPerToken[_account] = 1e18; _creditBalances[_account] = balance; - + // Globals _adjustGlobals(-oldCredits.toInt256(), balance.toInt256()); emit AccountRebasingDisabled(_account); } /** - * @dev Modify the supply without minting new tokens. This uses a change in - * the exchange rate between "credits" and OUSD tokens to change balances. + * @notice Distribute yield to users. This changes the exchange rate + * between "credits" and OUSD tokens to change rebasing user's balances. * @param _newTotalSupply New total supply of OUSD. */ function changeSupply(uint256 _newTotalSupply) external onlyVault { @@ -590,6 +539,10 @@ contract OUSD is Governable { ); } + /* + * @notice Send the yield from one account to another acount. + * Each account keeps their own balances. + */ function delegateYield(address from, address to) external onlyGovernor { require(from != address(0), "Zero from address not allowed"); require(to != address(0), "Zero to address not allowed"); @@ -639,11 +592,15 @@ contract OUSD is Governable { _creditBalances[from] = fromBalance; alternativeCreditsPerToken[from] = 1e18; _creditBalances[to] += creditsFrom; - + // Global _adjustGlobals(creditsFrom.toInt256(), -fromBalance.toInt256()); + emit YieldDelegated(from, to); } + /* + * @notice Stop sending the yield from one account to another acount. + */ function undelegateYield(address from) external onlyGovernor { // Require a delegation, which will also ensure a valid delegation require(yieldTo[from] != address(0), "Zero address not allowed"); @@ -660,12 +617,13 @@ contract OUSD is Governable { rebaseState[to] = RebaseOptions.StdRebasing; // Local + // alternativeCreditsPerToken[from] already 1e18 from `delegateYield()` _creditBalances[from] = fromBalance; - // no need to set the alternativeCreditsPerToken of the from account since - // that one is already set to 1e18 by the `delegateYield` function. + // alternativeCreditsPerToken[to] already 0 from `delegateYield()` _creditBalances[to] -= creditsFrom; - + // Global _adjustGlobals(-(creditsFrom).toInt256(), fromBalance.toInt256()); + emit YieldUndelegated(from, to); } } From 9ded07ad2f55e31fa050bb0f9c99adafd0326070 Mon Sep 17 00:00:00 2001 From: Daniel Von Fange Date: Wed, 20 Nov 2024 11:09:29 -0500 Subject: [PATCH 072/110] Comments spelling update --- contracts/contracts/token/OUSD.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 72135ee750..9fccce78e2 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -405,7 +405,7 @@ contract OUSD is Governable { */ function _autoMigrate(address _account) internal { bool isContract = _account.code.length > 0; - // In previous code versions, contracts would not have had thier + // In previous code versions, contracts would not have had their // rebaseState[_account] set to RebaseOptions.NonRebasing when migrated // therefor we check the actual accounting used on the account instead. if ( @@ -540,7 +540,7 @@ contract OUSD is Governable { } /* - * @notice Send the yield from one account to another acount. + * @notice Send the yield from one account to another account. * Each account keeps their own balances. */ function delegateYield(address from, address to) external onlyGovernor { @@ -599,7 +599,7 @@ contract OUSD is Governable { } /* - * @notice Stop sending the yield from one account to another acount. + * @notice Stop sending the yield from one account to another account. */ function undelegateYield(address from) external onlyGovernor { // Require a delegation, which will also ensure a valid delegation From 2c6bdc223da293878f5552157358148fe6a84b4d Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 20 Nov 2024 18:41:54 +0100 Subject: [PATCH 073/110] correct comments --- contracts/contracts/token/OUSD.sol | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 9338352f68..c816c76fb7 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -479,9 +479,8 @@ contract OUSD is Governable { /** * @notice Enable rebasing for an account. - * @dev Add a contract address to the non-rebasing exception list. The - * address's balance will be part of rebases and the account will be exposed - * to upside and downside. + * @dev Mark the account's `rebaseState` as `StdRebasing`. The address's balance + * will be part of rebases and the account will be exposed to the upside. * @param _account Address of the account. */ function governanceRebaseOptIn(address _account) external onlyGovernor { @@ -489,9 +488,9 @@ contract OUSD is Governable { } /** - * @dev Add a contract address to the non-rebasing exception list. The - * address's balance will be part of rebases and the account will be exposed - * to upside and downside. + * @notice Enable rebasing for the caller's account. + * @dev Mark the account's `rebaseState` as `StdRebasing`. The address's balance + * will be part of rebases and the account will be exposed to the upside. */ function rebaseOptIn() external { _rebaseOptIn(msg.sender); @@ -528,6 +527,11 @@ contract OUSD is Governable { emit AccountRebasingEnabled(_account); } + /** + * @notice Disable rebasing for the caller's account. + * @dev Mark the account's `rebaseState` as `StdNonRebasing`. The address's balance + * will not change on rebases. + */ function rebaseOptOut() external { _rebaseOptOut(msg.sender); } From f1939dbd966dfc0e8247e86bd51d393a4b7a241c Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 20 Nov 2024 22:11:19 +0100 Subject: [PATCH 074/110] unify variable names --- .../contracts/mocks/TestUpgradedOUSD.sol | 2 +- contracts/contracts/token/OUSD.sol | 188 +++++++++--------- 2 files changed, 95 insertions(+), 95 deletions(-) diff --git a/contracts/contracts/mocks/TestUpgradedOUSD.sol b/contracts/contracts/mocks/TestUpgradedOUSD.sol index 9d3555f9a2..5a6eaa7890 100644 --- a/contracts/contracts/mocks/TestUpgradedOUSD.sol +++ b/contracts/contracts/mocks/TestUpgradedOUSD.sol @@ -10,7 +10,7 @@ contract TestUpgradedOUSD is OUSD { function overwriteCreditBalances(address _account, uint256 _creditBalance) public { - _creditBalances[_account] = _creditBalance; + creditBalances[_account] = _creditBalance; } function overwriteAlternativeCPT(address _account, uint256 _acpt) public { diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 1a786ce9f3..35de2fa53b 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -42,11 +42,12 @@ contract OUSD is Governable { uint256[154] private _gap; // Slots to align with deployed contract uint256 private constant MAX_SUPPLY = type(uint128).max; uint256 public totalSupply; - mapping(address => mapping(address => uint256)) private _allowances; + mapping(address => mapping(address => uint256)) private allowances; address public vaultAddress; - mapping(address => uint256) internal _creditBalances; - uint256 private _rebasingCredits; // Sum of all rebasing credits (_creditBalances for rebasing accounts) - uint256 private _rebasingCreditsPerToken; + mapping(address => uint256) internal creditBalances; + // the 2 storage variables below need trailing underscores to not name collide with public functions + uint256 private rebasingCredits_; // Sum of all rebasing credits (creditBalances for rebasing accounts) + uint256 private rebasingCreditsPerToken_; uint256 public nonRebasingSupply; // All nonrebasing balances mapping(address => uint256) internal alternativeCreditsPerToken; mapping(address => RebaseOptions) public rebaseState; @@ -64,7 +65,7 @@ contract OUSD is Governable { require(_vaultAddress != address(0), "Zero vault address"); require(vaultAddress == address(0), "Already initialized"); - _rebasingCreditsPerToken = _initialCreditsPerToken; + rebasingCreditsPerToken_ = _initialCreditsPerToken; vaultAddress = _vaultAddress; } @@ -92,28 +93,28 @@ contract OUSD is Governable { * @return High resolution rebasingCreditsPerToken */ function rebasingCreditsPerTokenHighres() external view returns (uint256) { - return _rebasingCreditsPerToken; + return rebasingCreditsPerToken_; } /** * @return Low resolution rebasingCreditsPerToken */ function rebasingCreditsPerToken() external view returns (uint256) { - return _rebasingCreditsPerToken / RESOLUTION_INCREASE; + return rebasingCreditsPerToken_ / RESOLUTION_INCREASE; } /** * @return High resolution total number of rebasing credits */ function rebasingCreditsHighres() external view returns (uint256) { - return _rebasingCredits; + return rebasingCredits_; } /** * @return Low resolution total number of rebasing credits */ function rebasingCredits() external view returns (uint256) { - return _rebasingCredits / RESOLUTION_INCREASE; + return rebasingCredits_ / RESOLUTION_INCREASE; } /** @@ -127,13 +128,13 @@ contract OUSD is Governable { if (state == RebaseOptions.YieldDelegationSource) { // Saves a slot read when transferring to or from a yield delegating source // since we know creditBalances equals the balance. - return _creditBalances[_account]; + return creditBalances[_account]; } - uint256 baseBalance = (_creditBalances[_account] * 1e18) / + uint256 baseBalance = (creditBalances[_account] * 1e18) / _creditsPerToken(_account); if (state == RebaseOptions.YieldDelegationTarget) { - // _creditBalances of yieldFrom accounts equals token balances - return baseBalance - _creditBalances[yieldFrom[_account]]; + // creditBalances of yieldFrom accounts equals token balances + return baseBalance - creditBalances[yieldFrom[_account]]; } return baseBalance; } @@ -155,10 +156,10 @@ contract OUSD is Governable { // For a period before the resolution upgrade, we created all new // contract accounts at high resolution. Since they are not changing // as a result of this upgrade, we will return their true values - return (_creditBalances[_account], cpt); + return (creditBalances[_account], cpt); } else { return ( - _creditBalances[_account] / RESOLUTION_INCREASE, + creditBalances[_account] / RESOLUTION_INCREASE, cpt / RESOLUTION_INCREASE ); } @@ -180,7 +181,7 @@ contract OUSD is Governable { ) { return ( - _creditBalances[_account], + creditBalances[_account], _creditsPerToken(_account), true // all accounts have their resolution "upgraded" ); @@ -222,9 +223,10 @@ contract OUSD is Governable { uint256 _value ) external returns (bool) { require(_to != address(0), "Transfer to zero address"); - require(_value <= _allowances[_from][msg.sender], "Allowance exceeded"); + require(_value <= allowances[_from][msg.sender], "Allowance exceeded"); + + allowances[_from][msg.sender] -= _value; - _allowances[_from][msg.sender] -= _value; _executeTransfer(_from, _to, _value); emit Transfer(_from, _to, _value); @@ -251,64 +253,64 @@ contract OUSD is Governable { ); } - function _adjustAccount(address account, int256 balanceChange) + function _adjustAccount(address _account, int256 _balanceChange) internal returns (int256 rebasingCreditsDiff, int256 nonRebasingSupplyDiff) { - RebaseOptions state = rebaseState[account]; - int256 currentBalance = balanceOf(account).toInt256(); - if (currentBalance + balanceChange < 0) { + RebaseOptions state = rebaseState[_account]; + int256 currentBalance = balanceOf(_account).toInt256(); + if (currentBalance + _balanceChange < 0) { revert("Transfer amount exceeds balance"); } - uint256 newBalance = (currentBalance + balanceChange).toUint256(); + uint256 newBalance = (currentBalance + _balanceChange).toUint256(); if (state == RebaseOptions.YieldDelegationSource) { - address target = yieldTo[account]; + address target = yieldTo[_account]; uint256 targetOldBalance = balanceOf(target); uint256 targetNewCredits = _balanceToRebasingCredits( targetOldBalance + newBalance ); rebasingCreditsDiff = targetNewCredits.toInt256() - - _creditBalances[target].toInt256(); + creditBalances[target].toInt256(); - _creditBalances[account] = newBalance; - _creditBalances[target] = targetNewCredits; + creditBalances[_account] = newBalance; + creditBalances[target] = targetNewCredits; } else if (state == RebaseOptions.YieldDelegationTarget) { uint256 newCredits = _balanceToRebasingCredits( - newBalance + _creditBalances[yieldFrom[account]] + newBalance + creditBalances[yieldFrom[_account]] ); rebasingCreditsDiff = newCredits.toInt256() - - _creditBalances[account].toInt256(); - _creditBalances[account] = newCredits; + creditBalances[_account].toInt256(); + creditBalances[_account] = newCredits; } else { - _autoMigrate(account); - if (alternativeCreditsPerToken[account] > 0) { - nonRebasingSupplyDiff = balanceChange; - alternativeCreditsPerToken[account] = 1e18; - _creditBalances[account] = newBalance; + _autoMigrate(_account); + if (alternativeCreditsPerToken[_account] > 0) { + nonRebasingSupplyDiff = _balanceChange; + alternativeCreditsPerToken[_account] = 1e18; + creditBalances[_account] = newBalance; } else { uint256 newCredits = _balanceToRebasingCredits(newBalance); rebasingCreditsDiff = newCredits.toInt256() - - _creditBalances[account].toInt256(); - _creditBalances[account] = newCredits; + creditBalances[_account].toInt256(); + creditBalances[_account] = newCredits; } } } function _adjustGlobals( - int256 rebasingCreditsDiff, - int256 nonRebasingSupplyDiff + int256 _rebasingCreditsDiff, + int256 _nonRebasingSupplyDiff ) internal { - if (rebasingCreditsDiff != 0) { - _rebasingCredits = (_rebasingCredits.toInt256() + - rebasingCreditsDiff).toUint256(); + if (_rebasingCreditsDiff != 0) { + rebasingCredits_ = (rebasingCredits_.toInt256() + + _rebasingCreditsDiff).toUint256(); } - if (nonRebasingSupplyDiff != 0) { + if (_nonRebasingSupplyDiff != 0) { nonRebasingSupply = (nonRebasingSupply.toInt256() + - nonRebasingSupplyDiff).toUint256(); + _nonRebasingSupplyDiff).toUint256(); } } @@ -324,7 +326,7 @@ contract OUSD is Governable { view returns (uint256) { - return _allowances[_owner][_spender]; + return allowances[_owner][_spender]; } /** @@ -334,7 +336,7 @@ contract OUSD is Governable { * @param _value The amount of tokens to be spent. */ function approve(address _spender, uint256 _value) external returns (bool) { - _allowances[msg.sender][_spender] = _value; + allowances[msg.sender][_spender] = _value; emit Approval(msg.sender, _spender, _value); return true; } @@ -394,7 +396,7 @@ contract OUSD is Governable { if (alternativeCreditsPerToken[_account] != 0) { return alternativeCreditsPerToken[_account]; } else { - return _rebasingCreditsPerToken; + return rebasingCreditsPerToken_; } } @@ -417,7 +419,7 @@ contract OUSD is Governable { } } - function _balanceToRebasingCredits(uint256 balance) + function _balanceToRebasingCredits(uint256 _balance) internal view returns (uint256) @@ -426,7 +428,7 @@ contract OUSD is Governable { // at least the balance that they should have. // Note this should always be used on an absolute account value, // not on a possibly negative diff, because then the rounding would be wrong. - return ((balance) * _rebasingCreditsPerToken + 1e18 - 1) / 1e18; + return ((_balance) * rebasingCreditsPerToken_ + 1e18 - 1) / 1e18; } /** @@ -467,9 +469,9 @@ contract OUSD is Governable { // Account rebaseState[_account] = RebaseOptions.StdRebasing; alternativeCreditsPerToken[_account] = 0; - _creditBalances[_account] = _balanceToRebasingCredits(balance); + creditBalances[_account] = _balanceToRebasingCredits(balance); // Globals - _adjustGlobals(_creditBalances[_account].toInt256(), -balance.toInt256()); + _adjustGlobals(creditBalances[_account].toInt256(), -balance.toInt256()); emit AccountRebasingEnabled(_account); } @@ -492,13 +494,13 @@ contract OUSD is Governable { "Only standard rebasing accounts can opt out" ); - uint256 oldCredits = _creditBalances[_account]; + uint256 oldCredits = creditBalances[_account]; uint256 balance = balanceOf(_account); // Account rebaseState[_account] = RebaseOptions.StdNonRebasing; alternativeCreditsPerToken[_account] = 1e18; - _creditBalances[_account] = balance; + creditBalances[_account] = balance; // Globals _adjustGlobals(-oldCredits.toInt256(), balance.toInt256()); @@ -516,8 +518,8 @@ contract OUSD is Governable { if (totalSupply == _newTotalSupply) { emit TotalSupplyUpdatedHighres( totalSupply, - _rebasingCredits, - _rebasingCreditsPerToken + rebasingCredits_, + rebasingCreditsPerToken_ ); return; } @@ -526,16 +528,16 @@ contract OUSD is Governable { ? MAX_SUPPLY : _newTotalSupply; - _rebasingCreditsPerToken = - (_rebasingCredits * 1e18) / + rebasingCreditsPerToken_ = + (rebasingCredits_ * 1e18) / (totalSupply - nonRebasingSupply); - require(_rebasingCreditsPerToken > 0, "Invalid change in supply"); + require(rebasingCreditsPerToken_ > 0, "Invalid change in supply"); emit TotalSupplyUpdatedHighres( totalSupply, - _rebasingCredits, - _rebasingCreditsPerToken + rebasingCredits_, + rebasingCreditsPerToken_ ); } @@ -543,20 +545,20 @@ contract OUSD is Governable { * @notice Send the yield from one account to another account. * Each account keeps their own balances. */ - function delegateYield(address from, address to) external onlyGovernor { - require(from != address(0), "Zero from address not allowed"); - require(to != address(0), "Zero to address not allowed"); + function delegateYield(address _from, address _to) external onlyGovernor { + require(_from != address(0), "Zero from address not allowed"); + require(_to != address(0), "Zero to address not allowed"); - require(from != to, "Cannot delegate to self"); + require(_from != _to, "Cannot delegate to self"); require( - yieldFrom[to] == address(0) && - yieldTo[to] == address(0) && - yieldFrom[from] == address(0) && - yieldTo[from] == address(0), + yieldFrom[_to] == address(0) && + yieldTo[_to] == address(0) && + yieldFrom[_from] == address(0) && + yieldTo[_from] == address(0), "Blocked by existing yield delegation" ); - RebaseOptions stateFrom = rebaseState[from]; - RebaseOptions stateTo = rebaseState[to]; + RebaseOptions stateFrom = rebaseState[_from]; + RebaseOptions stateTo = rebaseState[_to]; require( stateFrom == RebaseOptions.NotSet || @@ -572,58 +574,56 @@ contract OUSD is Governable { "Invalid rebaseState to" ); - if (alternativeCreditsPerToken[from] == 0) { - _rebaseOptOut(from); + if (alternativeCreditsPerToken[_from] == 0) { + _rebaseOptOut(_from); } - if (alternativeCreditsPerToken[to] > 0) { - _rebaseOptIn(to); + if (alternativeCreditsPerToken[_to] > 0) { + _rebaseOptIn(_to); } - uint256 fromBalance = balanceOf(from); + uint256 fromBalance = balanceOf(_from); uint256 creditsFrom = _balanceToRebasingCredits(fromBalance); // Set up the bidirectional links - yieldTo[from] = to; - yieldFrom[to] = from; - rebaseState[from] = RebaseOptions.YieldDelegationSource; - rebaseState[to] = RebaseOptions.YieldDelegationTarget; + yieldTo[_from] = _to; + yieldFrom[_to] = _from; + rebaseState[_from] = RebaseOptions.YieldDelegationSource; + rebaseState[_to] = RebaseOptions.YieldDelegationTarget; // Local - _creditBalances[from] = fromBalance; - alternativeCreditsPerToken[from] = 1e18; - _creditBalances[to] += creditsFrom; + creditBalances[_from] = fromBalance; + alternativeCreditsPerToken[_from] = 1e18; + creditBalances[_to] += creditsFrom; // Global _adjustGlobals(creditsFrom.toInt256(), -fromBalance.toInt256()); - - emit YieldDelegated(from, to); + emit YieldDelegated(_from, _to); } /* * @notice Stop sending the yield from one account to another account. */ - function undelegateYield(address from) external onlyGovernor { + function undelegateYield(address _from) external onlyGovernor { // Require a delegation, which will also ensure a valid delegation - require(yieldTo[from] != address(0), "Zero address not allowed"); + require(yieldTo[_from] != address(0), "Zero address not allowed"); - address to = yieldTo[from]; - uint256 fromBalance = balanceOf(from); + address to = yieldTo[_from]; + uint256 fromBalance = balanceOf(_from); // these are credits of from account if it were rebasing uint256 creditsFrom = _balanceToRebasingCredits(fromBalance); // Remove the bidirectional links yieldFrom[to] = address(0); - yieldTo[from] = address(0); - rebaseState[from] = RebaseOptions.StdNonRebasing; + yieldTo[_from] = address(0); + rebaseState[_from] = RebaseOptions.StdNonRebasing; rebaseState[to] = RebaseOptions.StdRebasing; // Local // alternativeCreditsPerToken[from] already 1e18 from `delegateYield()` - _creditBalances[from] = fromBalance; + creditBalances[_from] = fromBalance; // alternativeCreditsPerToken[to] already 0 from `delegateYield()` - _creditBalances[to] -= creditsFrom; + creditBalances[to] -= creditsFrom; // Global _adjustGlobals(-(creditsFrom).toInt256(), fromBalance.toInt256()); - - emit YieldUndelegated(from, to); + emit YieldUndelegated(_from, to); } } From 57c87332d7aaa18fc83f100277d797eb938c0697 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 20 Nov 2024 22:29:05 +0100 Subject: [PATCH 075/110] make credits calculation based of off balance for higher accuracy (in context of rounding errors) --- contracts/contracts/token/OUSD.sol | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 35de2fa53b..8831dab9e9 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -582,7 +582,7 @@ contract OUSD is Governable { } uint256 fromBalance = balanceOf(_from); - uint256 creditsFrom = _balanceToRebasingCredits(fromBalance); + uint256 toBalance = balanceOf(_to); // Set up the bidirectional links yieldTo[_from] = _to; @@ -593,9 +593,9 @@ contract OUSD is Governable { // Local creditBalances[_from] = fromBalance; alternativeCreditsPerToken[_from] = 1e18; - creditBalances[_to] += creditsFrom; + creditBalances[_to] = _balanceToRebasingCredits(fromBalance + toBalance); // Global - _adjustGlobals(creditsFrom.toInt256(), -fromBalance.toInt256()); + _adjustGlobals(_balanceToRebasingCredits(fromBalance).toInt256(), -fromBalance.toInt256()); emit YieldDelegated(_from, _to); } @@ -608,8 +608,7 @@ contract OUSD is Governable { address to = yieldTo[_from]; uint256 fromBalance = balanceOf(_from); - // these are credits of from account if it were rebasing - uint256 creditsFrom = _balanceToRebasingCredits(fromBalance); + uint256 toBalance = balanceOf(to); // Remove the bidirectional links yieldFrom[to] = address(0); @@ -621,9 +620,9 @@ contract OUSD is Governable { // alternativeCreditsPerToken[from] already 1e18 from `delegateYield()` creditBalances[_from] = fromBalance; // alternativeCreditsPerToken[to] already 0 from `delegateYield()` - creditBalances[to] -= creditsFrom; + creditBalances[to] = _balanceToRebasingCredits(toBalance); // Global - _adjustGlobals(-(creditsFrom).toInt256(), fromBalance.toInt256()); + _adjustGlobals(-(_balanceToRebasingCredits(fromBalance).toInt256()), fromBalance.toInt256()); emit YieldUndelegated(_from, to); } } From dc803f2518ac3e5f06cb5eb040b875f90b4425bb Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 20 Nov 2024 22:31:55 +0100 Subject: [PATCH 076/110] minor gas optimisation --- contracts/contracts/token/OUSD.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 8831dab9e9..df664a1d65 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -447,12 +447,14 @@ contract OUSD is Governable { } function _rebaseOptIn(address _account) internal { + uint256 balance = balanceOf(_account); + // prettier-ignore require( alternativeCreditsPerToken[_account] > 0 || // Accounts may explicitly `rebaseOptIn` regardless of // accounting if they have a 0 balance. - balanceOf(_account) == 0 + balance == 0 , "Account must be non-rebasing" ); @@ -464,8 +466,6 @@ contract OUSD is Governable { "Only standard non-rebasing accounts can opt in" ); - uint256 balance = balanceOf(_account); - // Account rebaseState[_account] = RebaseOptions.StdRebasing; alternativeCreditsPerToken[_account] = 0; From 48931863587f50c5f550b88cbc7b87a392fda295 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Thu, 21 Nov 2024 11:59:28 +0100 Subject: [PATCH 077/110] correct storage slot amount so it totals to 200 --- contracts/contracts/token/OUSD.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index df664a1d65..b00a76facb 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -56,7 +56,7 @@ contract OUSD is Governable { mapping(address => address) public yieldFrom; uint256 private constant RESOLUTION_INCREASE = 1e9; - uint256[38] private __gap; // including below gap totals up to 200 + uint256[34] private __gap; // including below gap totals up to 200 function initialize(address _vaultAddress, uint256 _initialCreditsPerToken) external From 09bde1b43dafb81b0a60e7eabd1bd7ea669b4698 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Thu, 21 Nov 2024 14:06:51 +0100 Subject: [PATCH 078/110] Improve rebasing supply accuracy V2 (#2314) * accurate balance accounting v2 for nonRebasingSupply calculation * prettier * correct comment * minor gas optimisation --- contracts/contracts/token/OUSD.sol | 59 ++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 15 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index b00a76facb..643dfc2c99 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -38,7 +38,6 @@ contract OUSD is Governable { YieldDelegationTarget } - uint256[154] private _gap; // Slots to align with deployed contract uint256 private constant MAX_SUPPLY = type(uint128).max; uint256 public totalSupply; @@ -267,7 +266,7 @@ contract OUSD is Governable { if (state == RebaseOptions.YieldDelegationSource) { address target = yieldTo[_account]; uint256 targetOldBalance = balanceOf(target); - uint256 targetNewCredits = _balanceToRebasingCredits( + (uint256 targetNewCredits, ) = _balanceToRebasingCredits( targetOldBalance + newBalance ); rebasingCreditsDiff = @@ -277,7 +276,7 @@ contract OUSD is Governable { creditBalances[_account] = newBalance; creditBalances[target] = targetNewCredits; } else if (state == RebaseOptions.YieldDelegationTarget) { - uint256 newCredits = _balanceToRebasingCredits( + (uint256 newCredits, ) = _balanceToRebasingCredits( newBalance + creditBalances[yieldFrom[_account]] ); rebasingCreditsDiff = @@ -291,7 +290,7 @@ contract OUSD is Governable { alternativeCreditsPerToken[_account] = 1e18; creditBalances[_account] = newBalance; } else { - uint256 newCredits = _balanceToRebasingCredits(newBalance); + (uint256 newCredits, ) = _balanceToRebasingCredits(newBalance); rebasingCreditsDiff = newCredits.toInt256() - creditBalances[_account].toInt256(); @@ -315,7 +314,7 @@ contract OUSD is Governable { } /** - * @notice Function to check the amount of tokens that _owner has allowed + * @notice Function to check the amount of tokens that _owner has allowed * to `_spender`. * @param _owner The address which owns the funds. * @param _spender The address which will spend the funds. @@ -342,7 +341,7 @@ contract OUSD is Governable { } /** - * @notice Creates `_amount` tokens and assigns them to `_account`, + * @notice Creates `_amount` tokens and assigns them to `_account`, * increasing the total supply. */ function mint(address _account, uint256 _amount) external onlyVault { @@ -419,16 +418,27 @@ contract OUSD is Governable { } } + /** + * @dev Calculates credits from contract's global rebasingCreditsPerToken_, and + * also balance that corresponds to those credits. The latter is important + * when adjusting the contract's global nonRebasingSupply to circumvent any + * possible rounding errors. + * + * @param _balance Address of the account. + */ function _balanceToRebasingCredits(uint256 _balance) internal view - returns (uint256) + returns (uint256 rebasingCredits, uint256 balance) { // Rounds up, because we need to ensure that accounts always have // at least the balance that they should have. // Note this should always be used on an absolute account value, // not on a possibly negative diff, because then the rounding would be wrong. - return ((_balance) * rebasingCreditsPerToken_ + 1e18 - 1) / 1e18; + rebasingCredits = + ((_balance) * rebasingCreditsPerToken_ + 1e18 - 1) / + 1e18; + balance = (rebasingCredits * 1e18) / rebasingCreditsPerToken_; } /** @@ -448,7 +458,7 @@ contract OUSD is Governable { function _rebaseOptIn(address _account) internal { uint256 balance = balanceOf(_account); - + // prettier-ignore require( alternativeCreditsPerToken[_account] > 0 || @@ -469,9 +479,15 @@ contract OUSD is Governable { // Account rebaseState[_account] = RebaseOptions.StdRebasing; alternativeCreditsPerToken[_account] = 0; - creditBalances[_account] = _balanceToRebasingCredits(balance); + (uint256 newCredits, uint256 newBalance) = _balanceToRebasingCredits( + balance + ); + creditBalances[_account] = newCredits; // Globals - _adjustGlobals(creditBalances[_account].toInt256(), -balance.toInt256()); + _adjustGlobals( + newCredits.toInt256(), + -newBalance.toInt256() + ); emit AccountRebasingEnabled(_account); } @@ -593,9 +609,15 @@ contract OUSD is Governable { // Local creditBalances[_from] = fromBalance; alternativeCreditsPerToken[_from] = 1e18; - creditBalances[_to] = _balanceToRebasingCredits(fromBalance + toBalance); + (creditBalances[_to], ) = _balanceToRebasingCredits( + fromBalance + toBalance + ); // Global - _adjustGlobals(_balanceToRebasingCredits(fromBalance).toInt256(), -fromBalance.toInt256()); + ( + uint256 fromCredits, + uint256 fromBalanceAccurate + ) = _balanceToRebasingCredits(fromBalance); + _adjustGlobals(fromCredits.toInt256(), -fromBalanceAccurate.toInt256()); emit YieldDelegated(_from, _to); } @@ -620,9 +642,16 @@ contract OUSD is Governable { // alternativeCreditsPerToken[from] already 1e18 from `delegateYield()` creditBalances[_from] = fromBalance; // alternativeCreditsPerToken[to] already 0 from `delegateYield()` - creditBalances[to] = _balanceToRebasingCredits(toBalance); + (creditBalances[to], ) = _balanceToRebasingCredits(toBalance); // Global - _adjustGlobals(-(_balanceToRebasingCredits(fromBalance).toInt256()), fromBalance.toInt256()); + ( + uint256 fromCredits, + uint256 fromBalanceAccurate + ) = _balanceToRebasingCredits(fromBalance); + _adjustGlobals( + -(fromCredits.toInt256()), + fromBalanceAccurate.toInt256() + ); emit YieldUndelegated(_from, to); } } From ae51bbd49c3d9605780fe474de393cdfaae1db6f Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Thu, 21 Nov 2024 21:55:13 +0100 Subject: [PATCH 079/110] gas optimisation --- contracts/contracts/token/OUSD.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 643dfc2c99..1ba0c534bf 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -431,14 +431,15 @@ contract OUSD is Governable { view returns (uint256 rebasingCredits, uint256 balance) { + uint256 rebasingCreditsPerTokenMem = rebasingCreditsPerToken_; // Rounds up, because we need to ensure that accounts always have // at least the balance that they should have. // Note this should always be used on an absolute account value, // not on a possibly negative diff, because then the rounding would be wrong. rebasingCredits = - ((_balance) * rebasingCreditsPerToken_ + 1e18 - 1) / + ((_balance) * rebasingCreditsPerTokenMem + 1e18 - 1) / 1e18; - balance = (rebasingCredits * 1e18) / rebasingCreditsPerToken_; + balance = (rebasingCredits * 1e18) / rebasingCreditsPerTokenMem; } /** From f03deb32a46c9d7fb9fa4dd248d776cfac2499c8 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Thu, 21 Nov 2024 21:55:50 +0100 Subject: [PATCH 080/110] better naming --- contracts/contracts/token/OUSD.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 1ba0c534bf..36d806357f 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -429,7 +429,7 @@ contract OUSD is Governable { function _balanceToRebasingCredits(uint256 _balance) internal view - returns (uint256 rebasingCredits, uint256 balance) + returns (uint256 rebasingCredits, uint256 actualBalance) { uint256 rebasingCreditsPerTokenMem = rebasingCreditsPerToken_; // Rounds up, because we need to ensure that accounts always have @@ -439,7 +439,7 @@ contract OUSD is Governable { rebasingCredits = ((_balance) * rebasingCreditsPerTokenMem + 1e18 - 1) / 1e18; - balance = (rebasingCredits * 1e18) / rebasingCreditsPerTokenMem; + actualBalance = (rebasingCredits * 1e18) / rebasingCreditsPerTokenMem; } /** From db2044acc8da4eb01aa5df2d7bb0f82b5e8208e3 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Sat, 23 Nov 2024 00:23:38 +0100 Subject: [PATCH 081/110] add a test where multiple rebaseOptIn/OptOut calls do not result in increasing account balance --- contracts/test/token/ousd.js | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/contracts/test/token/ousd.js b/contracts/test/token/ousd.js index 70e87bf360..7bf9a67b1a 100644 --- a/contracts/test/token/ousd.js +++ b/contracts/test/token/ousd.js @@ -489,6 +489,26 @@ describe("Token", function () { expect(await ousd.totalSupply()).to.equal(totalSupplyBefore); }); + it("Calling rebaseOptIn / optOut in loop shouldn't keep increasing account's balance", async () => { + let { ousd, vault, matt, usdc, josh } = fixture; + + // Transfer USDC into the Vault to simulate yield + await usdc.connect(matt).transfer(vault.address, usdcUnits("200")); + await vault.rebase(); + + await ousd.connect(josh).rebaseOptOut(); + await ousd.connect(josh).rebaseOptIn(); + + const balanceBefore = await ousd.balanceOf(josh.address); + + for (let i = 0; i < 10 ; i++) { + await ousd.connect(josh).rebaseOptOut(); + await ousd.connect(josh).rebaseOptIn(); + } + + expect(await ousd.balanceOf(josh.address)).to.equal(balanceBefore); + }); + it("Should not allow EOA to call rebaseOptIn when already opted in to rebasing", async () => { let { ousd, matt, usdc } = fixture; await usdc.connect(matt).mint(usdcUnits("2")); @@ -502,7 +522,9 @@ describe("Token", function () { let { ousd, matt, usdc, josh } = fixture; await usdc.connect(matt).mint(usdcUnits("2")); // transfer all OUSD out - await ousd.connect(matt).transfer(josh.address, await ousd.balanceOf(matt.address)); + await ousd + .connect(matt) + .transfer(josh.address, await ousd.balanceOf(matt.address)); // user is allowed to override its NotSet rebasing state to Rebasing without negatively affecting // any of the token contract's invariants From 44951300cd2bb7eb1d387461465edbfc440f2779 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Sat, 23 Nov 2024 01:09:13 +0100 Subject: [PATCH 082/110] add a check for zero address with governanceRebaseOptIn tx --- contracts/contracts/token/OUSD.sol | 1 + contracts/test/token/ousd.js | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 36d806357f..96d3355c7b 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -447,6 +447,7 @@ contract OUSD is Governable { * @param _account Address of the account. */ function governanceRebaseOptIn(address _account) external onlyGovernor { + require(_account != address(0), "Zero address not allowed"); _rebaseOptIn(_account); } diff --git a/contracts/test/token/ousd.js b/contracts/test/token/ousd.js index 7bf9a67b1a..123589aa10 100644 --- a/contracts/test/token/ousd.js +++ b/contracts/test/token/ousd.js @@ -399,6 +399,20 @@ describe("Token", function () { ); }); + it("Should allow a governanceRebaseOptIn call", async () => { + let { ousd, governor, mockNonRebasing } = fixture; + await ousd.connect(governor).governanceRebaseOptIn(mockNonRebasing.address); + }); + + it("Should not allow a governanceRebaseOptIn of a zero address", async () => { + let { ousd, governor } = fixture; + await expect( + ousd + .connect(governor) + .governanceRebaseOptIn("0x0000000000000000000000000000000000000000") + ).to.be.revertedWith("Zero address not allowed"); + }); + it("Should maintain the correct balances when rebaseOptIn is called from non-rebasing contract", async () => { let { ousd, vault, matt, usdc, josh, mockNonRebasing } = fixture; @@ -501,11 +515,11 @@ describe("Token", function () { const balanceBefore = await ousd.balanceOf(josh.address); - for (let i = 0; i < 10 ; i++) { + for (let i = 0; i < 10; i++) { await ousd.connect(josh).rebaseOptOut(); await ousd.connect(josh).rebaseOptIn(); } - + expect(await ousd.balanceOf(josh.address)).to.equal(balanceBefore); }); From 53db807bfa802e7826d40ec55efdee964945829b Mon Sep 17 00:00:00 2001 From: Daniel Von Fange Date: Mon, 25 Nov 2024 08:58:21 -0500 Subject: [PATCH 083/110] Update on rebasing --- contracts/contracts/token/README-token-logic.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/contracts/contracts/token/README-token-logic.md b/contracts/contracts/token/README-token-logic.md index 5526abf134..dab52183b0 100644 --- a/contracts/contracts/token/README-token-logic.md +++ b/contracts/contracts/token/README-token-logic.md @@ -159,6 +159,8 @@ The token distributes yield to users by "rebasing" (changing supply). This leave The token is designed to gently degrade in resolutions once a huge amount of APY has been earned. Once this crosses a certain point, and enough resolution is no longer possible, transfers should slightly round up. +There is inevitable rounding error when rebasing, since there is no possible way to ensure that totalSupply is exactly the result of all the things that make it up. This is because totalSupply must be exactly equal to the new value and nonRebasingSupply must not change. The only option is to handle rounding errors by rounding down the rebasingCreditsPerToken. The resulting gap of undistributed yield is later distributed to users the next time the token rebases upwards. + ## Rebasing invariants @@ -195,6 +197,16 @@ The token is designed to gently degrade in resolutions once a huge amount of APY > A successful burn() call by the vault results in the target account's balance decreasing by the amount specified +## External integrations + +In production, the following things are true: + +- changeSupply can move up only. This is hardcoded into the vault. +- There will aways be 1e16+ dead rebasing tokens (we send them to a dead address at deploy time) + + + + [^1]: From the current code base. Historically there may be different data stored in storage slots. From 948014c7f08a915f9f5089fef758552aa8af9fea Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Sat, 23 Nov 2024 01:09:13 +0100 Subject: [PATCH 084/110] add a check for zero address with governanceRebaseOptIn tx --- contracts/contracts/token/OUSD.sol | 1 + contracts/test/token/ousd.js | 33 +++++++++++++++++++++++------- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 36d806357f..96d3355c7b 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -447,6 +447,7 @@ contract OUSD is Governable { * @param _account Address of the account. */ function governanceRebaseOptIn(address _account) external onlyGovernor { + require(_account != address(0), "Zero address not allowed"); _rebaseOptIn(_account); } diff --git a/contracts/test/token/ousd.js b/contracts/test/token/ousd.js index 7bf9a67b1a..7b7bda25f8 100644 --- a/contracts/test/token/ousd.js +++ b/contracts/test/token/ousd.js @@ -1,6 +1,6 @@ const { expect } = require("chai"); const { loadDefaultFixture, loadTokenTransferFixture } = require("../_fixture"); -const { utils } = require("ethers"); +const { utils, BigNumber } = require("ethers"); const { daiUnits, ousdUnits, usdcUnits, isFork } = require("../helpers"); @@ -94,8 +94,8 @@ describe("Token", function () { await vault.rebase(); // Credits per token should be the same for the contract - contractCreditsPerToken === - (await ousd.creditsBalanceOf(mockNonRebasing.address)); + const contractCreditsPerTokenAfter = await ousd.creditsBalanceOf(mockNonRebasing.address); + await expect(contractCreditsPerToken[1]).to.equal(contractCreditsPerTokenAfter[1]); // Validate rebasing and non rebasing credit accounting by calculating' // total supply manually @@ -103,9 +103,14 @@ describe("Token", function () { .mul(utils.parseUnits("1", 18)) .div(await ousd.rebasingCreditsPerTokenHighres()) .add(await ousd.nonRebasingSupply()); + await expect(calculatedTotalSupply).to.approxEqual( await ousd.totalSupply() ); + await expect(await ousd.rebasingCreditsPerTokenHighres()).to.approxEqualTolerance( + (await ousd.rebasingCreditsPerToken()).mul(BigNumber.from("1000000000")), + 0.01 // maxTolerancePct + ); }); it("Should transfer the correct amount from a rebasing account to a non-rebasing account with previously set creditsPerToken", async () => { @@ -267,8 +272,8 @@ describe("Token", function () { await usdc.connect(matt).transfer(vault.address, usdcUnits("200")); await vault.rebase(); // Credits per token should be the same for the contract - contractCreditsPerToken === - (await ousd.creditsBalanceOf(mockNonRebasing.address)); + const contractCreditsPerTokenAfter = await ousd.creditsBalanceOf(mockNonRebasing.address); + await expect(contractCreditsPerToken[1]).to.equal(contractCreditsPerTokenAfter[1]); // Validate rebasing and non rebasing credit accounting by calculating' // total supply manually @@ -399,6 +404,20 @@ describe("Token", function () { ); }); + it("Should allow a governanceRebaseOptIn call", async () => { + let { ousd, governor, mockNonRebasing } = fixture; + await ousd.connect(governor).governanceRebaseOptIn(mockNonRebasing.address); + }); + + it("Should not allow a governanceRebaseOptIn of a zero address", async () => { + let { ousd, governor } = fixture; + await expect( + ousd + .connect(governor) + .governanceRebaseOptIn("0x0000000000000000000000000000000000000000") + ).to.be.revertedWith("Zero address not allowed"); + }); + it("Should maintain the correct balances when rebaseOptIn is called from non-rebasing contract", async () => { let { ousd, vault, matt, usdc, josh, mockNonRebasing } = fixture; @@ -501,11 +520,11 @@ describe("Token", function () { const balanceBefore = await ousd.balanceOf(josh.address); - for (let i = 0; i < 10 ; i++) { + for (let i = 0; i < 10; i++) { await ousd.connect(josh).rebaseOptOut(); await ousd.connect(josh).rebaseOptIn(); } - + expect(await ousd.balanceOf(josh.address)).to.equal(balanceBefore); }); From 01b49a3be0545ba6ff74c79879a38adab03f01dd Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 25 Nov 2024 23:19:57 +0100 Subject: [PATCH 085/110] make an exception for balance exact non rebasing accounts (StdNonRebasing, YieldDelegationSource) --- contracts/contracts/token/OUSD.sol | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 96d3355c7b..7dfa81922b 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -156,6 +156,13 @@ contract OUSD is Governable { // contract accounts at high resolution. Since they are not changing // as a result of this upgrade, we will return their true values return (creditBalances[_account], cpt); + } else if (cpt == 1e18) { + // This is the current implementation's non rebasing account where cpt + // equals 1e18 and creditBalaces of the account equal the token balances + return ( + creditBalances[_account], + cpt + ); } else { return ( creditBalances[_account] / RESOLUTION_INCREASE, From e355f3da4abb05276e211d553c36e4b88c2fbfbd Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 25 Nov 2024 23:41:12 +0100 Subject: [PATCH 086/110] add test for the 1e27 cpt token exception --- contracts/test/token/ousd.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/contracts/test/token/ousd.js b/contracts/test/token/ousd.js index 7b7bda25f8..692ef7db86 100644 --- a/contracts/test/token/ousd.js +++ b/contracts/test/token/ousd.js @@ -961,5 +961,21 @@ describe("Token", function () { nonRebasingSupply.sub(await ousd.balanceOf(contract_account.address)) ).to.equal(await ousd.nonRebasingSupply()); }); + + it("Non rebasing accounts with cpt set to 1e27 should return value non corrected for resolution increase", async () => { + let { ousd, ousdUnlocked, rebase_eoa_notset_0, mockNonRebasing } = fixture; + + await ousd.connect(rebase_eoa_notset_0).transfer(mockNonRebasing.address, ousdUnits("10")); + // 10 * 1e27 + const _10_1e27 = BigNumber.from("100000000000000000000000000000"); + const _1e27 = BigNumber.from("1000000000000000000000000000"); + await ousdUnlocked.connect(rebase_eoa_notset_0).overwriteCreditBalances(mockNonRebasing.address, _10_1e27) + // 1e27 + await ousdUnlocked.connect(rebase_eoa_notset_0).overwriteAlternativeCPT(mockNonRebasing.address, _1e27) + + const contractCreditsPerToken = await ousd.creditsBalanceOf(mockNonRebasing.address); + await expect(contractCreditsPerToken[0]).to.equal(_10_1e27); + await expect(contractCreditsPerToken[1]).to.equal(_1e27); + }); }); }); From 7636c99c740f804798a32cfdf3114d8ada1996a4 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 25 Nov 2024 23:49:33 +0100 Subject: [PATCH 087/110] add a test to for creditsBalanceOf and creditsBalanceOfHighres --- contracts/test/token/ousd.js | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/contracts/test/token/ousd.js b/contracts/test/token/ousd.js index 692ef7db86..bb14bbf171 100644 --- a/contracts/test/token/ousd.js +++ b/contracts/test/token/ousd.js @@ -976,6 +976,25 @@ describe("Token", function () { const contractCreditsPerToken = await ousd.creditsBalanceOf(mockNonRebasing.address); await expect(contractCreditsPerToken[0]).to.equal(_10_1e27); await expect(contractCreditsPerToken[1]).to.equal(_1e27); - }); + }); + + it("Should report correct creditBalanceOf and creditsBalanceOfHighres", async () => { + let { ousd, ousdUnlocked, rebase_eoa_notset_0, mockNonRebasing } = fixture; + + await ousd.connect(rebase_eoa_notset_0).transfer(mockNonRebasing.address, ousdUnits("10")); + const _5_1e26 = BigNumber.from("500000000000000000000000000"); + const _5_1e17 = BigNumber.from("500000000000000000"); // 5 * 1e26 / RESOLUTION_INCREASE + await ousdUnlocked.connect(rebase_eoa_notset_0).overwriteCreditBalances(mockNonRebasing.address, _5_1e26) + // 1e27 + await ousdUnlocked.connect(rebase_eoa_notset_0).overwriteAlternativeCPT(mockNonRebasing.address, _5_1e26) + + const contractCreditsPerTokenHighres = await ousd.creditsBalanceOfHighres(mockNonRebasing.address); + await expect(contractCreditsPerTokenHighres[0]).to.equal(_5_1e26); + await expect(contractCreditsPerTokenHighres[1]).to.equal(_5_1e26); + + const contractCreditsPerToken = await ousd.creditsBalanceOf(mockNonRebasing.address); + await expect(contractCreditsPerToken[0]).to.equal(_5_1e17); + await expect(contractCreditsPerToken[1]).to.equal(_5_1e17); + }); }); }); From da68531534b98de9cdfe129f1276959a6107a4e5 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 25 Nov 2024 23:51:32 +0100 Subject: [PATCH 088/110] add nonRebasingCreditsPerToken to the test --- contracts/test/token/ousd.js | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/test/token/ousd.js b/contracts/test/token/ousd.js index bb14bbf171..faa7e60d97 100644 --- a/contracts/test/token/ousd.js +++ b/contracts/test/token/ousd.js @@ -991,6 +991,7 @@ describe("Token", function () { const contractCreditsPerTokenHighres = await ousd.creditsBalanceOfHighres(mockNonRebasing.address); await expect(contractCreditsPerTokenHighres[0]).to.equal(_5_1e26); await expect(contractCreditsPerTokenHighres[1]).to.equal(_5_1e26); + await expect(await ousd.nonRebasingCreditsPerToken(mockNonRebasing.address)).to.equal(_5_1e26); const contractCreditsPerToken = await ousd.creditsBalanceOf(mockNonRebasing.address); await expect(contractCreditsPerToken[0]).to.equal(_5_1e17); From 1b9d1e39a9901a5c574f051e1c6ba6198ab2b019 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 26 Nov 2024 00:46:46 +0100 Subject: [PATCH 089/110] add auto migration test and revert test for rebaseOptOut --- contracts/test/token/ousd.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/contracts/test/token/ousd.js b/contracts/test/token/ousd.js index faa7e60d97..24c0892669 100644 --- a/contracts/test/token/ousd.js +++ b/contracts/test/token/ousd.js @@ -939,10 +939,6 @@ describe("Token", function () { await expect(matt).has.an.approxBalanceOf("100.00", ousd); await expect(anna).has.an.balanceOf("90", ousd); }); - - it("should be able to chenge yield delegation", async () => { - //A Should delegate to account B, rebase with profit, delegate to account C and all have correct balances - }); }); describe("Old code migrated contract accounts", function () { @@ -997,5 +993,20 @@ describe("Token", function () { await expect(contractCreditsPerToken[0]).to.equal(_5_1e17); await expect(contractCreditsPerToken[1]).to.equal(_5_1e17); }); + + it("Contract should auto migrate to StdNonRebasing", async () => { + let { ousd, nonrebase_cotract_notSet_0, rebase_eoa_notset_0} = fixture; + + await expect(await ousd.rebaseState(nonrebase_cotract_notSet_0.address)).to.equal(0); // NotSet + await ousd.connect(rebase_eoa_notset_0).transfer(nonrebase_cotract_notSet_0.address, ousdUnits("10")); + await expect(await ousd.rebaseState(nonrebase_cotract_notSet_0.address)).to.equal(1); // StdNonRebasing + }); + + it("Yield delegating account should not rebase opt out", async () => { + let { ousd, rebase_delegate_target_0 } = fixture; + await expect(ousd.connect(rebase_delegate_target_0).rebaseOptOut()).to.be.revertedWith( + "Only standard rebasing accounts can opt out" + ); + }); }); }); From 34021fb777ffb7a63a7fdbaaff60c94cfe1b1d2f Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 26 Nov 2024 01:15:27 +0100 Subject: [PATCH 090/110] prettier --- contracts/test/token/token-transfers.js | 140 +++++++++++++----------- 1 file changed, 74 insertions(+), 66 deletions(-) diff --git a/contracts/test/token/token-transfers.js b/contracts/test/token/token-transfers.js index 5c3e0fdc59..c8d87bc0ee 100644 --- a/contracts/test/token/token-transfers.js +++ b/contracts/test/token/token-transfers.js @@ -215,7 +215,7 @@ describe("Account type variations", function () { balancePartOfRebasingCredits: true, inYieldDelegation: true, }, - { + { name: "rebase_delegate_target_1", balancePartOfRebasingCredits: true, inYieldDelegation: true, @@ -235,44 +235,47 @@ describe("Account type variations", function () { const { name: toName, balancePartOfRebasingCredits: toAffectsRC } = toAccounts[j]; - (skipTransferTest ? it.skip : it)(`Should transfer from ${fromName} to ${toName}`, async () => { - const fromAccount = fixture[fromName]; - const toAccount = fixture[toName]; - const { ousd } = fixture; + (skipTransferTest ? it.skip : it)( + `Should transfer from ${fromName} to ${toName}`, + async () => { + const fromAccount = fixture[fromName]; + const toAccount = fixture[toName]; + const { ousd } = fixture; - const fromBalance = await ousd.balanceOf(fromAccount.address); - const toBalance = await ousd.balanceOf(toAccount.address); - // Random transfer between 2-8 - const amount = ousdUnits(`${2 + Math.random() * 6}`); + const fromBalance = await ousd.balanceOf(fromAccount.address); + const toBalance = await ousd.balanceOf(toAccount.address); + // Random transfer between 2-8 + const amount = ousdUnits(`${2 + Math.random() * 6}`); - if (isContract) { - await fromAccount.transfer(toAccount.address, amount); - } else { - await ousd.connect(fromAccount).transfer(toAccount.address, amount); - } + if (isContract) { + await fromAccount.transfer(toAccount.address, amount); + } else { + await ousd.connect(fromAccount).transfer(toAccount.address, amount); + } - // check balances - await expect(await ousd.balanceOf(fromAccount.address)).to.equal( - fromBalance.sub(amount) - ); - await expect(await ousd.balanceOf(toAccount.address)).to.equal( - toBalance.add(amount) - ); + // check balances + await expect(await ousd.balanceOf(fromAccount.address)).to.equal( + fromBalance.sub(amount) + ); + await expect(await ousd.balanceOf(toAccount.address)).to.equal( + toBalance.add(amount) + ); - let expectedNonRebasingSupply = nonRebasingSupply; - if (!fromAffectsRC) { - expectedNonRebasingSupply = expectedNonRebasingSupply.sub(amount); - } - if (!toAffectsRC) { - expectedNonRebasingSupply = expectedNonRebasingSupply.add(amount); - } + let expectedNonRebasingSupply = nonRebasingSupply; + if (!fromAffectsRC) { + expectedNonRebasingSupply = expectedNonRebasingSupply.sub(amount); + } + if (!toAffectsRC) { + expectedNonRebasingSupply = expectedNonRebasingSupply.add(amount); + } - // check global contract (in)variants - await expect(await ousd.totalSupply()).to.equal(totalSupply); - await expect(await ousd.nonRebasingSupply()).to.equal( - expectedNonRebasingSupply - ); - }); + // check global contract (in)variants + await expect(await ousd.totalSupply()).to.equal(totalSupply); + await expect(await ousd.nonRebasingSupply()).to.equal( + expectedNonRebasingSupply + ); + } + ); } } @@ -286,45 +289,50 @@ describe("Account type variations", function () { const { name: toName, balancePartOfRebasingCredits: toBalancePartOfRC, - inYieldDelegation: inYieldDelegationTarget + inYieldDelegation: inYieldDelegationTarget, } = toAccounts[j]; - (inYieldDelegationSource || inYieldDelegationTarget ? it.skip : it)(`Non rebasing supply should be correct when ${fromName} delegates to ${toName}`, async () => { - const fromAccount = fixture[fromName]; - const toAccount = fixture[toName]; - const { ousd, governor } = fixture; + (inYieldDelegationSource || inYieldDelegationTarget ? it.skip : it)( + `Non rebasing supply should be correct when ${fromName} delegates to ${toName}`, + async () => { + const fromAccount = fixture[fromName]; + const toAccount = fixture[toName]; + const { ousd, governor } = fixture; - const fromBalance = await ousd.balanceOf(fromAccount.address); - const toBalance = await ousd.balanceOf(toAccount.address); - let expectedNonRebasingSupply = nonRebasingSupply; + const fromBalance = await ousd.balanceOf(fromAccount.address); + const toBalance = await ousd.balanceOf(toAccount.address); + let expectedNonRebasingSupply = nonRebasingSupply; - await ousd - .connect(governor) - .delegateYield(fromAccount.address, toAccount.address); + await ousd + .connect(governor) + .delegateYield(fromAccount.address, toAccount.address); - // check balances haven't changed - await expect(await ousd.balanceOf(fromAccount.address)).to.equal( - fromBalance - ); - await expect(await ousd.balanceOf(toAccount.address)).to.equal( - toBalance - ); + // check balances haven't changed + await expect(await ousd.balanceOf(fromAccount.address)).to.equal( + fromBalance + ); + await expect(await ousd.balanceOf(toAccount.address)).to.equal( + toBalance + ); - // account was non rebasing and became rebasing - if (!fromBalancePartOfRC) { - expectedNonRebasingSupply = expectedNonRebasingSupply.sub(fromBalance); - } - // account was non rebasing and became rebasing - if (!toBalancePartOfRC) { - expectedNonRebasingSupply = expectedNonRebasingSupply.sub(toBalance); - } + // account was non rebasing and became rebasing + if (!fromBalancePartOfRC) { + expectedNonRebasingSupply = + expectedNonRebasingSupply.sub(fromBalance); + } + // account was non rebasing and became rebasing + if (!toBalancePartOfRC) { + expectedNonRebasingSupply = + expectedNonRebasingSupply.sub(toBalance); + } - // check global contract (in)variants - await expect(await ousd.totalSupply()).to.equal(totalSupply); - await expect(await ousd.nonRebasingSupply()).to.equal( - expectedNonRebasingSupply - ); - }); + // check global contract (in)variants + await expect(await ousd.totalSupply()).to.equal(totalSupply); + await expect(await ousd.nonRebasingSupply()).to.equal( + expectedNonRebasingSupply + ); + } + ); } } }); From 57cccbafb8ac2928b776a1dd2e7aa65911eb83cc Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 26 Nov 2024 01:15:56 +0100 Subject: [PATCH 091/110] add tests for missing requires in yield delegation --- contracts/contracts/token/OUSD.sol | 10 +-- contracts/test/token/ousd.js | 132 ++++++++++++++++++++++------- 2 files changed, 104 insertions(+), 38 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 7dfa81922b..a8d97b9aac 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -159,10 +159,7 @@ contract OUSD is Governable { } else if (cpt == 1e18) { // This is the current implementation's non rebasing account where cpt // equals 1e18 and creditBalaces of the account equal the token balances - return ( - creditBalances[_account], - cpt - ); + return (creditBalances[_account], cpt); } else { return ( creditBalances[_account] / RESOLUTION_INCREASE, @@ -493,10 +490,7 @@ contract OUSD is Governable { ); creditBalances[_account] = newCredits; // Globals - _adjustGlobals( - newCredits.toInt256(), - -newBalance.toInt256() - ); + _adjustGlobals(newCredits.toInt256(), -newBalance.toInt256()); emit AccountRebasingEnabled(_account); } diff --git a/contracts/test/token/ousd.js b/contracts/test/token/ousd.js index 24c0892669..88dcf5f9ed 100644 --- a/contracts/test/token/ousd.js +++ b/contracts/test/token/ousd.js @@ -4,6 +4,8 @@ const { utils, BigNumber } = require("ethers"); const { daiUnits, ousdUnits, usdcUnits, isFork } = require("../helpers"); +const zeroAddress = "0x0000000000000000000000000000000000000000"; + describe("Token", function () { if (isFork) { this.timeout(0); @@ -26,9 +28,7 @@ describe("Token", function () { it("Should return 0 balance for the zero address", async () => { const { ousd } = fixture; - expect( - await ousd.balanceOf("0x0000000000000000000000000000000000000000") - ).to.equal(0); + expect(await ousd.balanceOf(zeroAddress)).to.equal(0); }); it("Should not allow anyone to mint OUSD directly", async () => { @@ -94,8 +94,12 @@ describe("Token", function () { await vault.rebase(); // Credits per token should be the same for the contract - const contractCreditsPerTokenAfter = await ousd.creditsBalanceOf(mockNonRebasing.address); - await expect(contractCreditsPerToken[1]).to.equal(contractCreditsPerTokenAfter[1]); + const contractCreditsPerTokenAfter = await ousd.creditsBalanceOf( + mockNonRebasing.address + ); + await expect(contractCreditsPerToken[1]).to.equal( + contractCreditsPerTokenAfter[1] + ); // Validate rebasing and non rebasing credit accounting by calculating' // total supply manually @@ -107,7 +111,9 @@ describe("Token", function () { await expect(calculatedTotalSupply).to.approxEqual( await ousd.totalSupply() ); - await expect(await ousd.rebasingCreditsPerTokenHighres()).to.approxEqualTolerance( + await expect( + await ousd.rebasingCreditsPerTokenHighres() + ).to.approxEqualTolerance( (await ousd.rebasingCreditsPerToken()).mul(BigNumber.from("1000000000")), 0.01 // maxTolerancePct ); @@ -272,8 +278,12 @@ describe("Token", function () { await usdc.connect(matt).transfer(vault.address, usdcUnits("200")); await vault.rebase(); // Credits per token should be the same for the contract - const contractCreditsPerTokenAfter = await ousd.creditsBalanceOf(mockNonRebasing.address); - await expect(contractCreditsPerToken[1]).to.equal(contractCreditsPerTokenAfter[1]); + const contractCreditsPerTokenAfter = await ousd.creditsBalanceOf( + mockNonRebasing.address + ); + await expect(contractCreditsPerToken[1]).to.equal( + contractCreditsPerTokenAfter[1] + ); // Validate rebasing and non rebasing credit accounting by calculating' // total supply manually @@ -412,9 +422,7 @@ describe("Token", function () { it("Should not allow a governanceRebaseOptIn of a zero address", async () => { let { ousd, governor } = fixture; await expect( - ousd - .connect(governor) - .governanceRebaseOptIn("0x0000000000000000000000000000000000000000") + ousd.connect(governor).governanceRebaseOptIn(zeroAddress) ).to.be.revertedWith("Zero address not allowed"); }); @@ -939,6 +947,30 @@ describe("Token", function () { await expect(matt).has.an.approxBalanceOf("100.00", ousd); await expect(anna).has.an.balanceOf("90", ousd); }); + + it("Should not delegate yield from a zero address", async () => { + let { ousd, governor, matt } = fixture; + + await expect( + ousd.connect(governor).delegateYield(zeroAddress, matt.address) + ).to.be.revertedWith("Zero from address not allowed"); + }); + + it("Should not delegate yield to a zero address", async () => { + let { ousd, governor, matt } = fixture; + + await expect( + ousd.connect(governor).delegateYield(matt.address, zeroAddress) + ).to.be.revertedWith("Zero to address not allowed"); + }); + + it("Should not delegate yield to self", async () => { + let { ousd, governor, matt } = fixture; + + await expect( + ousd.connect(governor).delegateYield(matt.address, matt.address) + ).to.be.revertedWith("Cannot delegate to self"); + }); }); describe("Old code migrated contract accounts", function () { @@ -959,54 +991,94 @@ describe("Token", function () { }); it("Non rebasing accounts with cpt set to 1e27 should return value non corrected for resolution increase", async () => { - let { ousd, ousdUnlocked, rebase_eoa_notset_0, mockNonRebasing } = fixture; + let { ousd, ousdUnlocked, rebase_eoa_notset_0, mockNonRebasing } = + fixture; - await ousd.connect(rebase_eoa_notset_0).transfer(mockNonRebasing.address, ousdUnits("10")); + await ousd + .connect(rebase_eoa_notset_0) + .transfer(mockNonRebasing.address, ousdUnits("10")); // 10 * 1e27 const _10_1e27 = BigNumber.from("100000000000000000000000000000"); const _1e27 = BigNumber.from("1000000000000000000000000000"); - await ousdUnlocked.connect(rebase_eoa_notset_0).overwriteCreditBalances(mockNonRebasing.address, _10_1e27) + await ousdUnlocked + .connect(rebase_eoa_notset_0) + .overwriteCreditBalances(mockNonRebasing.address, _10_1e27); // 1e27 - await ousdUnlocked.connect(rebase_eoa_notset_0).overwriteAlternativeCPT(mockNonRebasing.address, _1e27) + await ousdUnlocked + .connect(rebase_eoa_notset_0) + .overwriteAlternativeCPT(mockNonRebasing.address, _1e27); - const contractCreditsPerToken = await ousd.creditsBalanceOf(mockNonRebasing.address); + const contractCreditsPerToken = await ousd.creditsBalanceOf( + mockNonRebasing.address + ); await expect(contractCreditsPerToken[0]).to.equal(_10_1e27); await expect(contractCreditsPerToken[1]).to.equal(_1e27); }); it("Should report correct creditBalanceOf and creditsBalanceOfHighres", async () => { - let { ousd, ousdUnlocked, rebase_eoa_notset_0, mockNonRebasing } = fixture; + let { ousd, ousdUnlocked, rebase_eoa_notset_0, mockNonRebasing } = + fixture; - await ousd.connect(rebase_eoa_notset_0).transfer(mockNonRebasing.address, ousdUnits("10")); + await ousd + .connect(rebase_eoa_notset_0) + .transfer(mockNonRebasing.address, ousdUnits("10")); const _5_1e26 = BigNumber.from("500000000000000000000000000"); const _5_1e17 = BigNumber.from("500000000000000000"); // 5 * 1e26 / RESOLUTION_INCREASE - await ousdUnlocked.connect(rebase_eoa_notset_0).overwriteCreditBalances(mockNonRebasing.address, _5_1e26) + await ousdUnlocked + .connect(rebase_eoa_notset_0) + .overwriteCreditBalances(mockNonRebasing.address, _5_1e26); // 1e27 - await ousdUnlocked.connect(rebase_eoa_notset_0).overwriteAlternativeCPT(mockNonRebasing.address, _5_1e26) + await ousdUnlocked + .connect(rebase_eoa_notset_0) + .overwriteAlternativeCPT(mockNonRebasing.address, _5_1e26); - const contractCreditsPerTokenHighres = await ousd.creditsBalanceOfHighres(mockNonRebasing.address); + const contractCreditsPerTokenHighres = await ousd.creditsBalanceOfHighres( + mockNonRebasing.address + ); await expect(contractCreditsPerTokenHighres[0]).to.equal(_5_1e26); await expect(contractCreditsPerTokenHighres[1]).to.equal(_5_1e26); - await expect(await ousd.nonRebasingCreditsPerToken(mockNonRebasing.address)).to.equal(_5_1e26); + await expect( + await ousd.nonRebasingCreditsPerToken(mockNonRebasing.address) + ).to.equal(_5_1e26); - const contractCreditsPerToken = await ousd.creditsBalanceOf(mockNonRebasing.address); + const contractCreditsPerToken = await ousd.creditsBalanceOf( + mockNonRebasing.address + ); await expect(contractCreditsPerToken[0]).to.equal(_5_1e17); await expect(contractCreditsPerToken[1]).to.equal(_5_1e17); }); it("Contract should auto migrate to StdNonRebasing", async () => { - let { ousd, nonrebase_cotract_notSet_0, rebase_eoa_notset_0} = fixture; + let { ousd, nonrebase_cotract_notSet_0, rebase_eoa_notset_0 } = fixture; - await expect(await ousd.rebaseState(nonrebase_cotract_notSet_0.address)).to.equal(0); // NotSet - await ousd.connect(rebase_eoa_notset_0).transfer(nonrebase_cotract_notSet_0.address, ousdUnits("10")); - await expect(await ousd.rebaseState(nonrebase_cotract_notSet_0.address)).to.equal(1); // StdNonRebasing + await expect( + await ousd.rebaseState(nonrebase_cotract_notSet_0.address) + ).to.equal(0); // NotSet + await ousd + .connect(rebase_eoa_notset_0) + .transfer(nonrebase_cotract_notSet_0.address, ousdUnits("10")); + await expect( + await ousd.rebaseState(nonrebase_cotract_notSet_0.address) + ).to.equal(1); // StdNonRebasing }); it("Yield delegating account should not rebase opt out", async () => { let { ousd, rebase_delegate_target_0 } = fixture; - await expect(ousd.connect(rebase_delegate_target_0).rebaseOptOut()).to.be.revertedWith( - "Only standard rebasing accounts can opt out" - ); + await expect( + ousd.connect(rebase_delegate_target_0).rebaseOptOut() + ).to.be.revertedWith("Only standard rebasing accounts can opt out"); + }); + + it("Should not un-delegate yield from a zero address or address not part of yield delegation", async () => { + let { ousd, rebase_eoa_notset_0, governor } = fixture; + + await expect( + ousd.connect(governor).undelegateYield(zeroAddress) + ).to.be.revertedWith("Zero address not allowed"); + + await expect( + ousd.connect(governor).undelegateYield(rebase_eoa_notset_0.address) + ).to.be.revertedWith("Zero address not allowed"); }); }); }); From 24bf72a3a07c7c4d01e435b698af48c381615f21 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 26 Nov 2024 11:11:15 +0100 Subject: [PATCH 092/110] simplify code --- contracts/contracts/token/OUSD.sol | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index a8d97b9aac..b08e6ee70e 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -151,14 +151,13 @@ contract OUSD is Governable { returns (uint256, uint256) { uint256 cpt = _creditsPerToken(_account); - if (cpt == 1e27) { - // For a period before the resolution upgrade, we created all new - // contract accounts at high resolution. Since they are not changing - // as a result of this upgrade, we will return their true values - return (creditBalances[_account], cpt); - } else if (cpt == 1e18) { - // This is the current implementation's non rebasing account where cpt - // equals 1e18 and creditBalaces of the account equal the token balances + if (cpt == 1e27 || cpt == 1e18) { + // There are 2 reasons why we return the non downscaled amounts: + // - 1e27 For a period before the resolution upgrade, we created all new + // contract accounts at high resolution. Since they are not changing + // as a result of this upgrade, we will return their true values + // - 1e18 This is the current implementation's non rebasing account where cpt + // equals 1e18 and creditBalaces of the account equal the token balances return (creditBalances[_account], cpt); } else { return ( From f1b52903dd1964ecfea87f75739f4d16547ea697 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 26 Nov 2024 17:23:31 +0100 Subject: [PATCH 093/110] revert to the previous implementation of the deprecated function --- contracts/contracts/token/OUSD.sol | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index b08e6ee70e..96041cb953 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -151,13 +151,10 @@ contract OUSD is Governable { returns (uint256, uint256) { uint256 cpt = _creditsPerToken(_account); - if (cpt == 1e27 || cpt == 1e18) { - // There are 2 reasons why we return the non downscaled amounts: - // - 1e27 For a period before the resolution upgrade, we created all new - // contract accounts at high resolution. Since they are not changing - // as a result of this upgrade, we will return their true values - // - 1e18 This is the current implementation's non rebasing account where cpt - // equals 1e18 and creditBalaces of the account equal the token balances + if (cpt == 1e27) { + // For a period before the resolution upgrade, we created all new + // contract accounts at high resolution. Since they are not changing + // as a result of this upgrade, we will return their true values return (creditBalances[_account], cpt); } else { return ( From 20f1bd19ee4de348dcd62c6d71331603d1c7a123 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Thu, 28 Nov 2024 13:18:01 +0100 Subject: [PATCH 094/110] add OETH upgrade deployment file --- contracts/deploy/mainnet/110_oeth_upgrade.js | 38 ++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 contracts/deploy/mainnet/110_oeth_upgrade.js diff --git a/contracts/deploy/mainnet/110_oeth_upgrade.js b/contracts/deploy/mainnet/110_oeth_upgrade.js new file mode 100644 index 0000000000..f49e2a87ce --- /dev/null +++ b/contracts/deploy/mainnet/110_oeth_upgrade.js @@ -0,0 +1,38 @@ +const { deploymentWithGovernanceProposal } = require("../../utils/deploy"); + +module.exports = deploymentWithGovernanceProposal( + { + deployName: "110_oeth_upgrade", + forceDeploy: false, + //forceSkip: true, + reduceQueueTime: true, + deployerIsProposer: false, + proposalId: "", + }, + async ({ deployWithConfirmation }) => { + // Deployer Actions + // ---------------- + const cOETHProxy = await ethers.getContract("OETHProxy"); + + // Deploy new version of OETH contract + const dOETHImpl = await deployWithConfirmation("OETH", []); + + // Governance Actions + // ---------------- + return { + name: "Upgrade OETH token contract\n\ + \n\ + This upgrade enabled yield delegation controlled by xOGN governance \n\ + \n\ + ", + actions: [ + // Upgrade the OETH token proxy contract to the new implementation + { + contract: cOETHProxy, + signature: "upgradeTo(address)", + args: [dOETHImpl.address], + } + ], + }; + } +); From 7b76cb89c77e49534c23385ae76d3ef0909f7bd2 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Fri, 29 Nov 2024 15:21:40 +0100 Subject: [PATCH 095/110] change license to Business Source License --- contracts/contracts/token/OUSD.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 96041cb953..abd54780ea 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.0; /** From dbb3434ff5233c7eed336542943320bc6bff47a7 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Fri, 29 Nov 2024 15:47:00 +0100 Subject: [PATCH 096/110] prettier --- contracts/deploy/mainnet/110_oeth_upgrade.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/deploy/mainnet/110_oeth_upgrade.js b/contracts/deploy/mainnet/110_oeth_upgrade.js index f49e2a87ce..f14af4d3f0 100644 --- a/contracts/deploy/mainnet/110_oeth_upgrade.js +++ b/contracts/deploy/mainnet/110_oeth_upgrade.js @@ -31,7 +31,7 @@ module.exports = deploymentWithGovernanceProposal( contract: cOETHProxy, signature: "upgradeTo(address)", args: [dOETHImpl.address], - } + }, ], }; } From 93545c3f8df3cc01279aab309bd7dbf0ad5ecf46 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Fri, 29 Nov 2024 17:53:49 +0100 Subject: [PATCH 097/110] on changeSupply round up in the favour of the protocol --- contracts/contracts/token/OUSD.sol | 6 ++++-- contracts/test/token/ousd.js | 25 +++++++++++++++++++++---- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index abd54780ea..8bd07380bf 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -543,9 +543,11 @@ contract OUSD is Governable { ? MAX_SUPPLY : _newTotalSupply; + uint256 rebasingSupply = totalSupply - nonRebasingSupply; + // round up in the favour of the protocol rebasingCreditsPerToken_ = - (rebasingCredits_ * 1e18) / - (totalSupply - nonRebasingSupply); + (rebasingCredits_ * 1e18 + rebasingSupply - 1) / + rebasingSupply; require(rebasingCreditsPerToken_ > 0, "Invalid change in supply"); diff --git a/contracts/test/token/ousd.js b/contracts/test/token/ousd.js index 88dcf5f9ed..4c48cd7245 100644 --- a/contracts/test/token/ousd.js +++ b/contracts/test/token/ousd.js @@ -463,9 +463,12 @@ describe("Token", function () { .div(utils.parseUnits("1", 18)) .add(1); - await expect(rebasingCredits).to.equal( - initialRebasingCredits.add(creditsAdded) + const resultingCredits = initialRebasingCredits.add(creditsAdded); + // when calling changeSupply(rebase) OUSD contract can round down by 1 WEI. + await expect(rebasingCredits).to.gte( + resultingCredits.sub(BigNumber.from("1")) ); + await expect(rebasingCredits).to.lte(resultingCredits); expect(await ousd.totalSupply()).to.approxEqual( initialTotalSupply.add(utils.parseUnits("200", 18)) @@ -695,9 +698,23 @@ describe("Token", function () { // Contract originally contained $200, now has $202. // Matt should have (99/200) * 202 OUSD - await expect(matt).has.a.balanceOf("99.99", ousd); + // because rebase rounds down in protocol's favour resulting user balances can be off by 1 WEI + const mattExpectedBalance = ousdUnits("99.99"); + await expect(await ousd.balanceOf(matt.address)).to.be.gte( + mattExpectedBalance.sub(BigNumber.from("1")) + ); + await expect(await ousd.balanceOf(matt.address)).to.be.lte( + mattExpectedBalance + ); + + const annaExpectedBalance = ousdUnits("1.01"); // Anna should have (1/200) * 202 OUSD - await expect(anna).has.a.balanceOf("1.01", ousd); + await expect(await ousd.balanceOf(anna.address)).to.be.gte( + annaExpectedBalance.sub(BigNumber.from("1")) + ); + await expect(await ousd.balanceOf(anna.address)).to.be.lte( + annaExpectedBalance + ); }); it("Should mint correct amounts on non-rebasing account without previously set creditsPerToken", async () => { From dc854bcbf99959ecbba0d37f336e2b6e98d1d12a Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 2 Dec 2024 00:50:31 +0100 Subject: [PATCH 098/110] round down when calculating credits from balances --- contracts/contracts/token/OUSD.sol | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 8bd07380bf..21880e350e 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -432,13 +432,8 @@ contract OUSD is Governable { returns (uint256 rebasingCredits, uint256 actualBalance) { uint256 rebasingCreditsPerTokenMem = rebasingCreditsPerToken_; - // Rounds up, because we need to ensure that accounts always have - // at least the balance that they should have. - // Note this should always be used on an absolute account value, - // not on a possibly negative diff, because then the rounding would be wrong. - rebasingCredits = - ((_balance) * rebasingCreditsPerTokenMem + 1e18 - 1) / - 1e18; + // Round down in favour of the protocol + rebasingCredits = ((_balance) * rebasingCreditsPerTokenMem) / 1e18; actualBalance = (rebasingCredits * 1e18) / rebasingCreditsPerTokenMem; } From 63ee2ba0c5f3b96b9c672f3d67d07c4cd5981cef Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 2 Dec 2024 13:57:58 +0100 Subject: [PATCH 099/110] Revert "round down when calculating credits from balances" This reverts commit dc854bcbf99959ecbba0d37f336e2b6e98d1d12a. --- contracts/contracts/token/OUSD.sol | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 21880e350e..8bd07380bf 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -432,8 +432,13 @@ contract OUSD is Governable { returns (uint256 rebasingCredits, uint256 actualBalance) { uint256 rebasingCreditsPerTokenMem = rebasingCreditsPerToken_; - // Round down in favour of the protocol - rebasingCredits = ((_balance) * rebasingCreditsPerTokenMem) / 1e18; + // Rounds up, because we need to ensure that accounts always have + // at least the balance that they should have. + // Note this should always be used on an absolute account value, + // not on a possibly negative diff, because then the rounding would be wrong. + rebasingCredits = + ((_balance) * rebasingCreditsPerTokenMem + 1e18 - 1) / + 1e18; actualBalance = (rebasingCredits * 1e18) / rebasingCreditsPerTokenMem; } From 296f5f4c8cf5e3a251ca7df789714b838866740b Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Thu, 5 Dec 2024 14:43:19 +0100 Subject: [PATCH 100/110] fix typos (#2323) --- contracts/contracts/token/OUSD.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 8bd07380bf..f2339074b3 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -408,7 +408,7 @@ contract OUSD is Governable { bool isContract = _account.code.length > 0; // In previous code versions, contracts would not have had their // rebaseState[_account] set to RebaseOptions.NonRebasing when migrated - // therefor we check the actual accounting used on the account instead. + // therefore we check the actual accounting used on the account instead. if ( isContract && rebaseState[_account] == RebaseOptions.NotSet && @@ -424,7 +424,7 @@ contract OUSD is Governable { * when adjusting the contract's global nonRebasingSupply to circumvent any * possible rounding errors. * - * @param _balance Address of the account. + * @param _balance Balance of the account. */ function _balanceToRebasingCredits(uint256 _balance) internal @@ -560,7 +560,7 @@ contract OUSD is Governable { /* * @notice Send the yield from one account to another account. - * Each account keeps their own balances. + * Each account keeps its own balances. */ function delegateYield(address _from, address _to) external onlyGovernor { require(_from != address(0), "Zero from address not allowed"); From 74bd138cdeafea9293ec0b4d06e3f2ec2fba199d Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Thu, 5 Dec 2024 14:49:16 +0100 Subject: [PATCH 101/110] gas optimisation (#2322) --- contracts/contracts/token/OUSD.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index f2339074b3..33a65ef3b6 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -392,8 +392,9 @@ contract OUSD is Governable { view returns (uint256) { - if (alternativeCreditsPerToken[_account] != 0) { - return alternativeCreditsPerToken[_account]; + uint256 alternativeCreditsPerTokenMem = alternativeCreditsPerToken[_account]; + if (alternativeCreditsPerTokenMem != 0) { + return alternativeCreditsPerTokenMem; } else { return rebasingCreditsPerToken_; } From 040ad7adaf0cd2e78e920eab973920223d1bcf9a Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Thu, 5 Dec 2024 14:49:59 +0100 Subject: [PATCH 102/110] add missing natspec (#2321) --- contracts/contracts/token/OUSD.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 33a65ef3b6..3fb628a2f7 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -215,6 +215,7 @@ contract OUSD is Governable { * @param _from The address you want to send tokens from. * @param _to The address you want to transfer to. * @param _value The amount of tokens to be transferred. + * @return true on success. */ function transferFrom( address _from, @@ -333,6 +334,7 @@ contract OUSD is Governable { * tokens on behalf of msg.sender. * @param _spender The address which will spend the funds. * @param _value The amount of tokens to be spent. + * @return true on success. */ function approve(address _spender, uint256 _value) external returns (bool) { allowances[msg.sender][_spender] = _value; From d9c6defda36538b907dc968e23b5d4ee547c3657 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Thu, 5 Dec 2024 22:39:27 +0100 Subject: [PATCH 103/110] L-02 Missing Docstrings (#2319) * add missing natspec * corrections to code comments --- contracts/contracts/token/OUSD.sol | 49 +++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 3fb628a2f7..a747e8e675 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -14,20 +14,47 @@ contract OUSD is Governable { using SafeCast for int256; using SafeCast for uint256; + /// @dev Event triggered when the supply changes + /// @param totalSupply Updated token total supply + /// @param rebasingCredits Updated token rebasing credits + /// @param rebasingCreditsPerToken Updated token rebasing credits per token event TotalSupplyUpdatedHighres( uint256 totalSupply, uint256 rebasingCredits, uint256 rebasingCreditsPerToken ); + /// @dev Event triggered when an account opts in for rebasing + /// @param account Address of the account event AccountRebasingEnabled(address account); + /// @dev Event triggered when an account opts out of rebasing + /// @param account Address of the account event AccountRebasingDisabled(address account); + /// @dev Emitted when `value` tokens are moved from one account `from` to + /// another `to`. + /// @param from Address of the account tokens are moved from + /// @param to Address of the account tokens are moved to + /// @param value Amount of tokens transferred event Transfer(address indexed from, address indexed to, uint256 value); + /// @dev Emitted when the allowance of a `spender` for an `owner` is set by + /// a call to {approve}. `value` is the new allowance. + /// @param owner Address of the owner approving allowance + /// @param spender Address of the spender allowance is granted to + /// @param value Amount of tokens spender can transfer event Approval( address indexed owner, address indexed spender, uint256 value ); + /// @dev Yield resulting from {changeSupply} that a `source` account would + /// receive is directed to `target` account. + /// @param source Address of the source forwarding the yield + /// @param target Address of the target receiving the yield event YieldDelegated(address source, address target); + /// @dev Yield delegation from `source` account to the `target` account is + /// suspended. + /// @param source Address of the source suspending yield forwarding + /// @param target Address of the target no longer receiving yield from `source` + /// account event YieldUndelegated(address source, address target); enum RebaseOptions { @@ -40,23 +67,39 @@ contract OUSD is Governable { uint256[154] private _gap; // Slots to align with deployed contract uint256 private constant MAX_SUPPLY = type(uint128).max; + /// @dev The amount of tokens in existence uint256 public totalSupply; mapping(address => mapping(address => uint256)) private allowances; + /// @dev The vault with privileges to execute {mint}, {burn} + /// and {changeSupply} address public vaultAddress; mapping(address => uint256) internal creditBalances; // the 2 storage variables below need trailing underscores to not name collide with public functions uint256 private rebasingCredits_; // Sum of all rebasing credits (creditBalances for rebasing accounts) uint256 private rebasingCreditsPerToken_; - uint256 public nonRebasingSupply; // All nonrebasing balances + /// @dev The amount of tokens that are not rebasing - receiving yield + uint256 public nonRebasingSupply; mapping(address => uint256) internal alternativeCreditsPerToken; + /// @dev A map of all addresses and their respective RebaseOptions mapping(address => RebaseOptions) public rebaseState; mapping(address => uint256) private __deprecated_isUpgraded; + /// @dev A map of addresses that have yields forwarded to. This is an + /// inverse mapping of {yieldFrom} + /// Key Account forwarding yield + /// Value Account receiving yield mapping(address => address) public yieldTo; + /// @dev A map of addresses that are receiving the yield. This is an + /// inverse mapping of {yieldTo} + /// Key Account receiving yield + /// Value Account forwarding yield mapping(address => address) public yieldFrom; uint256 private constant RESOLUTION_INCREASE = 1e9; uint256[34] private __gap; // including below gap totals up to 200 + /// @dev Initializes the contract and sets necessary variables. + /// @param _vaultAddress Address of the vault contract + /// @param _initialCreditsPerToken The starting rebasing credits per token. function initialize(address _vaultAddress, uint256 _initialCreditsPerToken) external onlyGovernor @@ -68,14 +111,18 @@ contract OUSD is Governable { vaultAddress = _vaultAddress; } + /// @dev Returns the symbol of the token, a shorter version + /// of the name. function symbol() external pure virtual returns (string memory) { return "OUSD"; } + /// @dev Returns the name of the token. function name() external pure virtual returns (string memory) { return "Origin Dollar"; } + /// @dev Returns the number of decimals used to get its user representation. function decimals() external pure virtual returns (uint8) { return 18; } From 5e57112a9db3bed51d5ef725a589ca89f576df45 Mon Sep 17 00:00:00 2001 From: Daniel Von Fange Date: Tue, 10 Dec 2024 09:16:38 -0500 Subject: [PATCH 104/110] Correct globals storage --- contracts/contracts/token/OUSD.sol | 62 +++++++++++++----------------- 1 file changed, 26 insertions(+), 36 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index a747e8e675..6435c966eb 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -314,7 +314,7 @@ contract OUSD is Governable { if (state == RebaseOptions.YieldDelegationSource) { address target = yieldTo[_account]; uint256 targetOldBalance = balanceOf(target); - (uint256 targetNewCredits, ) = _balanceToRebasingCredits( + uint256 targetNewCredits = _balanceToRebasingCredits( targetOldBalance + newBalance ); rebasingCreditsDiff = @@ -324,7 +324,7 @@ contract OUSD is Governable { creditBalances[_account] = newBalance; creditBalances[target] = targetNewCredits; } else if (state == RebaseOptions.YieldDelegationTarget) { - (uint256 newCredits, ) = _balanceToRebasingCredits( + uint256 newCredits = _balanceToRebasingCredits( newBalance + creditBalances[yieldFrom[_account]] ); rebasingCreditsDiff = @@ -338,7 +338,7 @@ contract OUSD is Governable { alternativeCreditsPerToken[_account] = 1e18; creditBalances[_account] = newBalance; } else { - (uint256 newCredits, ) = _balanceToRebasingCredits(newBalance); + uint256 newCredits = _balanceToRebasingCredits(newBalance); rebasingCreditsDiff = newCredits.toInt256() - creditBalances[_account].toInt256(); @@ -479,17 +479,13 @@ contract OUSD is Governable { function _balanceToRebasingCredits(uint256 _balance) internal view - returns (uint256 rebasingCredits, uint256 actualBalance) + returns (uint256 rebasingCredits) { - uint256 rebasingCreditsPerTokenMem = rebasingCreditsPerToken_; // Rounds up, because we need to ensure that accounts always have // at least the balance that they should have. // Note this should always be used on an absolute account value, // not on a possibly negative diff, because then the rounding would be wrong. - rebasingCredits = - ((_balance) * rebasingCreditsPerTokenMem + 1e18 - 1) / - 1e18; - actualBalance = (rebasingCredits * 1e18) / rebasingCreditsPerTokenMem; + return ((_balance) * rebasingCreditsPerToken_ + 1e18 - 1) / 1e18; } /** @@ -528,15 +524,14 @@ contract OUSD is Governable { "Only standard non-rebasing accounts can opt in" ); + uint256 newCredits = _balanceToRebasingCredits(balance); + // Account rebaseState[_account] = RebaseOptions.StdRebasing; alternativeCreditsPerToken[_account] = 0; - (uint256 newCredits, uint256 newBalance) = _balanceToRebasingCredits( - balance - ); creditBalances[_account] = newCredits; // Globals - _adjustGlobals(newCredits.toInt256(), -newBalance.toInt256()); + _adjustGlobals(newCredits.toInt256(), -balance.toInt256()); emit AccountRebasingEnabled(_account); } @@ -650,25 +645,23 @@ contract OUSD is Governable { uint256 fromBalance = balanceOf(_from); uint256 toBalance = balanceOf(_to); + uint256 oldToCredits = creditBalances[_to]; + uint256 newToCredits = _balanceToRebasingCredits(fromBalance + toBalance); // Set up the bidirectional links yieldTo[_from] = _to; yieldFrom[_to] = _from; + + // Local rebaseState[_from] = RebaseOptions.YieldDelegationSource; + alternativeCreditsPerToken[_from] = 1e18; + creditBalances[_from] = fromBalance; rebaseState[_to] = RebaseOptions.YieldDelegationTarget; + creditBalances[_to] = newToCredits; - // Local - creditBalances[_from] = fromBalance; - alternativeCreditsPerToken[_from] = 1e18; - (creditBalances[_to], ) = _balanceToRebasingCredits( - fromBalance + toBalance - ); // Global - ( - uint256 fromCredits, - uint256 fromBalanceAccurate - ) = _balanceToRebasingCredits(fromBalance); - _adjustGlobals(fromCredits.toInt256(), -fromBalanceAccurate.toInt256()); + int256 creditsChange = newToCredits.toInt256() - oldToCredits.toInt256(); + _adjustGlobals(creditsChange, -(fromBalance).toInt256()); emit YieldDelegated(_from, _to); } @@ -682,27 +675,24 @@ contract OUSD is Governable { address to = yieldTo[_from]; uint256 fromBalance = balanceOf(_from); uint256 toBalance = balanceOf(to); + uint256 oldToCredits = creditBalances[to]; + uint256 newToCredits = _balanceToRebasingCredits(toBalance); // Remove the bidirectional links yieldFrom[to] = address(0); yieldTo[_from] = address(0); - rebaseState[_from] = RebaseOptions.StdNonRebasing; - rebaseState[to] = RebaseOptions.StdRebasing; - + // Local + rebaseState[_from] = RebaseOptions.StdNonRebasing; // alternativeCreditsPerToken[from] already 1e18 from `delegateYield()` creditBalances[_from] = fromBalance; + rebaseState[to] = RebaseOptions.StdRebasing; // alternativeCreditsPerToken[to] already 0 from `delegateYield()` - (creditBalances[to], ) = _balanceToRebasingCredits(toBalance); + creditBalances[to] = newToCredits; + // Global - ( - uint256 fromCredits, - uint256 fromBalanceAccurate - ) = _balanceToRebasingCredits(fromBalance); - _adjustGlobals( - -(fromCredits.toInt256()), - fromBalanceAccurate.toInt256() - ); + int256 creditsChange = newToCredits.toInt256() - oldToCredits.toInt256(); + _adjustGlobals(creditsChange, fromBalance.toInt256()); emit YieldUndelegated(_from, to); } } From eb957f1f98820f89a3d68fdbc0e470fa30f84af3 Mon Sep 17 00:00:00 2001 From: Daniel Von Fange Date: Tue, 17 Dec 2024 11:56:05 -0500 Subject: [PATCH 105/110] Only empty accounts can rebaseOptIn if already rebasing --- contracts/contracts/token/OUSD.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 6435c966eb..d2c2a896cc 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -512,7 +512,7 @@ contract OUSD is Governable { alternativeCreditsPerToken[_account] > 0 || // Accounts may explicitly `rebaseOptIn` regardless of // accounting if they have a 0 balance. - balance == 0 + creditBalances[_account] == 0 , "Account must be non-rebasing" ); From d285b3273e4b3a9ca69062f35429647f311be547 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 17 Dec 2024 21:19:09 +0100 Subject: [PATCH 106/110] gas optimisation --- contracts/contracts/token/OUSD.sol | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index d2c2a896cc..dd5136f4cf 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -270,9 +270,13 @@ contract OUSD is Governable { uint256 _value ) external returns (bool) { require(_to != address(0), "Transfer to zero address"); - require(_value <= allowances[_from][msg.sender], "Allowance exceeded"); + uint256 userAllowance = allowances[_from][msg.sender]; + require(_value <= userAllowance, "Allowance exceeded"); - allowances[_from][msg.sender] -= _value; + + unchecked { + allowances[_from][msg.sender] = userAllowance - _value; + } _executeTransfer(_from, _to, _value); From 55efcd6b6abe8d1764a349473fd3ade21dd35caf Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 17 Dec 2024 22:28:33 +0100 Subject: [PATCH 107/110] remove extra new line --- contracts/contracts/token/OUSD.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index dd5136f4cf..fef835706f 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -273,7 +273,6 @@ contract OUSD is Governable { uint256 userAllowance = allowances[_from][msg.sender]; require(_value <= userAllowance, "Allowance exceeded"); - unchecked { allowances[_from][msg.sender] = userAllowance - _value; } From c9806079d464574d0ed58496be0f7ac958df4708 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 17 Dec 2024 23:55:28 +0100 Subject: [PATCH 108/110] optimise gas when setting alternativeCreditsPerToken (#2325) --- contracts/contracts/token/OUSD.sol | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index fef835706f..9c9f87b750 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -336,9 +336,14 @@ contract OUSD is Governable { creditBalances[_account] = newCredits; } else { _autoMigrate(_account); - if (alternativeCreditsPerToken[_account] > 0) { + uint256 alternativeCreditsPerTokenMem = alternativeCreditsPerToken[ + _account + ]; + if (alternativeCreditsPerTokenMem > 0) { nonRebasingSupplyDiff = _balanceChange; - alternativeCreditsPerToken[_account] = 1e18; + if (alternativeCreditsPerTokenMem != 1e18) { + alternativeCreditsPerToken[_account] = 1e18; + } creditBalances[_account] = newBalance; } else { uint256 newCredits = _balanceToRebasingCredits(newBalance); @@ -444,7 +449,9 @@ contract OUSD is Governable { view returns (uint256) { - uint256 alternativeCreditsPerTokenMem = alternativeCreditsPerToken[_account]; + uint256 alternativeCreditsPerTokenMem = alternativeCreditsPerToken[ + _account + ]; if (alternativeCreditsPerTokenMem != 0) { return alternativeCreditsPerTokenMem; } else { From 07b3e70838c1d846920802a9a687a9ee178acdb4 Mon Sep 17 00:00:00 2001 From: Roy-Certora <101589076+Roy-Certora@users.noreply.github.com> Date: Thu, 19 Dec 2024 16:38:37 +0200 Subject: [PATCH 109/110] Certora Formal Verification for OUSD (#2329) * Add OUSD spec files and running scripts * Small updates to running scripts. * Add invariants to SumOfBalances spec. --- .gitignore | 3 + certora/confs/OUSD_accounting.conf | 16 ++ certora/confs/OUSD_balances.conf | 18 ++ certora/confs/OUSD_other.conf | 17 ++ certora/confs/OUSD_sumOfBalances.conf | 19 ++ certora/run.sh | 4 + certora/specs/OUSD/AccountInvariants.spec | 158 +++++++++++ certora/specs/OUSD/BalanceInvariants.spec | 183 +++++++++++++ certora/specs/OUSD/OtherInvariants.spec | 312 ++++++++++++++++++++++ certora/specs/OUSD/SumOfBalances.spec | 195 ++++++++++++++ certora/specs/OUSD/common.spec | 92 +++++++ 11 files changed, 1017 insertions(+) create mode 100644 certora/confs/OUSD_accounting.conf create mode 100644 certora/confs/OUSD_balances.conf create mode 100644 certora/confs/OUSD_other.conf create mode 100644 certora/confs/OUSD_sumOfBalances.conf create mode 100644 certora/run.sh create mode 100644 certora/specs/OUSD/AccountInvariants.spec create mode 100644 certora/specs/OUSD/BalanceInvariants.spec create mode 100644 certora/specs/OUSD/OtherInvariants.spec create mode 100644 certora/specs/OUSD/SumOfBalances.spec create mode 100644 certora/specs/OUSD/common.spec diff --git a/.gitignore b/.gitignore index bfc271b61d..05a805c264 100644 --- a/.gitignore +++ b/.gitignore @@ -84,3 +84,6 @@ fork-coverage unit-coverage .VSCodeCounter + +# Certora # +.certora_internal diff --git a/certora/confs/OUSD_accounting.conf b/certora/confs/OUSD_accounting.conf new file mode 100644 index 0000000000..7bdc7cf7ed --- /dev/null +++ b/certora/confs/OUSD_accounting.conf @@ -0,0 +1,16 @@ +{ + "files": [ + "contracts/contracts/token/OUSD.sol" + ], + "msg": "OUSD Accounting invariants", + "packages": [ + "@openzeppelin/contracts=contracts/lib/openzeppelin/contracts", + ], + "process": "emv", + "prover_args": [ + + ], + "solc": "solc8.28", + "verify": "OUSD:certora/specs/OUSD/AccountInvariants.spec", + "server": "production", +} \ No newline at end of file diff --git a/certora/confs/OUSD_balances.conf b/certora/confs/OUSD_balances.conf new file mode 100644 index 0000000000..c1c8d283f7 --- /dev/null +++ b/certora/confs/OUSD_balances.conf @@ -0,0 +1,18 @@ +{ + "files": [ + "contracts/contracts/token/OUSD.sol" + ], + "msg": "OUSD Balances invariants", + "packages": [ + "@openzeppelin/contracts=contracts/lib/openzeppelin/contracts", + ], + "process": "emv", + "prover_args": [ + "-mediumTimeout 40", + ], + "multi_assert_check":true, + "smt_timeout":"1000", + "solc": "solc8.28", + "verify": "OUSD:certora/specs/OUSD/BalanceInvariants.spec", + "server": "production" +} \ No newline at end of file diff --git a/certora/confs/OUSD_other.conf b/certora/confs/OUSD_other.conf new file mode 100644 index 0000000000..cc28a77c16 --- /dev/null +++ b/certora/confs/OUSD_other.conf @@ -0,0 +1,17 @@ +{ + "files": [ + "contracts/contracts/token/OUSD.sol" + ], + "msg": "OUSD other invariants", + "packages": [ + "@openzeppelin/contracts=contracts/lib/openzeppelin/contracts", + ], + "process": "emv", + "prover_args": [ + "-mediumTimeout 40", + ], + "smt_timeout":"1000", + "solc": "solc8.28", + "verify": "OUSD:certora/specs/OUSD/OtherInvariants.spec", + "server": "production" +} \ No newline at end of file diff --git a/certora/confs/OUSD_sumOfBalances.conf b/certora/confs/OUSD_sumOfBalances.conf new file mode 100644 index 0000000000..34d9d30c47 --- /dev/null +++ b/certora/confs/OUSD_sumOfBalances.conf @@ -0,0 +1,19 @@ +{ + "files": [ + "contracts/contracts/token/OUSD.sol" + ], + "msg": "OUSD Sum of balances rules", + "packages": [ + "@openzeppelin/contracts=contracts/lib/openzeppelin/contracts", + ], + "process": "emv", + "prover_args": [ + "-mediumTimeout 40", + "-s [z3:lia2,z3:arith1,yices:def]", + ], + "multi_assert_check":true, + "smt_timeout":"1000", + "solc": "solc8.28", + "verify": "OUSD:certora/specs/OUSD/SumOfBalances.spec", + "server": "production" +} \ No newline at end of file diff --git a/certora/run.sh b/certora/run.sh new file mode 100644 index 0000000000..c6c57a4140 --- /dev/null +++ b/certora/run.sh @@ -0,0 +1,4 @@ +certoraRun certora/confs/OUSD_accounting.conf +certoraRun certora/confs/OUSD_balances.conf +certoraRun certora/confs/OUSD_other.conf +certoraRun certora/confs/OUSD_sumOfBalances.conf diff --git a/certora/specs/OUSD/AccountInvariants.spec b/certora/specs/OUSD/AccountInvariants.spec new file mode 100644 index 0000000000..1de654903e --- /dev/null +++ b/certora/specs/OUSD/AccountInvariants.spec @@ -0,0 +1,158 @@ +import "./common.spec"; + +function allAccountValidState() { + requireInvariant DelegationAccountsCorrelation(); + requireInvariant DelegationValidRebaseState(); + requireInvariant stdNonRebasingDoesntYield(); + requireInvariant alternativeCreditsPerTokenIsOneOrZeroOnly(); + requireInvariant yieldDelegationSourceHasNonZeroYeildTo(); + requireInvariant yieldDelegationTargetHasNonZeroYeildFrom(); + requireInvariant yieldToOfZeroIsZero(); + requireInvariant yieldFromOfZeroIsZero(); + requireInvariant cantYieldToSelf(); + requireInvariant cantYieldFromSelf(); + requireInvariant zeroAlternativeCreditsPerTokenStates(); + requireInvariant nonZeroAlternativeCreditsPerTokenStates(); +} + +/// @title Any non zero valued YieldTo points to an account that has a YieldFrom pointing back to the starting account and vice versa. +/// @property Account Invariants +invariant DelegationAccountsCorrelation() + forall address account. + (OUSD.yieldTo[account] != 0 => (OUSD.yieldFrom[OUSD.yieldTo[account]] == account)) + && + (OUSD.yieldFrom[account] != 0 => (OUSD.yieldTo[OUSD.yieldFrom[account]] == account)) + { + preserved with (env e) { + requireInvariant DelegationValidRebaseState(); + requireInvariant yieldToOfZeroIsZero(); + requireInvariant yieldFromOfZeroIsZero(); + } + } + +/// @title Any non zero valued YieldTo points to an account Iff that account is in YieldDelegationSource state and +/// Any non zero valued YieldFrom points to an account Iff that account is in YieldDelegationTarget state. +/// @property Account Invariants +invariant DelegationValidRebaseState() + forall address account. + (OUSD.yieldTo[account] != 0 <=> OUSD.rebaseState[account] == YieldDelegationSource()) + && + (OUSD.yieldFrom[account] != 0 <=> OUSD.rebaseState[account] == YieldDelegationTarget()) + { + preserved with (env e) { + requireInvariant DelegationAccountsCorrelation(); + requireInvariant yieldToOfZeroIsZero(); + requireInvariant yieldFromOfZeroIsZero(); + } + } + +/// @title Any account with a zero value in alternativeCreditsPerToken has a rebaseState that is one of (NotSet, StdRebasing, or YieldDelegationTarget) +/// @property Account Invariants +invariant zeroAlternativeCreditsPerTokenStates() + forall address account . OUSD.alternativeCreditsPerToken[account] == 0 <=> (OUSD.rebaseState[account] == NotSet() || + OUSD.rebaseState[account] == StdRebasing() || + OUSD.rebaseState[account] == YieldDelegationTarget()) + { + preserved { + requireInvariant yieldToOfZeroIsZero(); + requireInvariant yieldFromOfZeroIsZero(); + requireInvariant DelegationAccountsCorrelation(); + requireInvariant DelegationValidRebaseState(); + } + } + +/// @title Any account with value of 1e18 in alternativeCreditsPerToken has a rebaseState that is one of (StdNonRebasing, YieldDelegationSource) +/// @property Account Invariants +invariant nonZeroAlternativeCreditsPerTokenStates() + forall address account . OUSD.alternativeCreditsPerToken[account] != 0 <=> (OUSD.rebaseState[account] == StdNonRebasing() || + OUSD.rebaseState[account] == YieldDelegationSource()) + { + preserved undelegateYield(address _account) with (env e) { + requireInvariant yieldToOfZeroIsZero(); + requireInvariant yieldFromOfZeroIsZero(); + requireInvariant DelegationAccountsCorrelation(); + requireInvariant DelegationValidRebaseState(); + } + } + +/// @title The result of balanceOf of any account in StdNonRebasing state equals the account's credit-balance. +/// @property Balance Invariants +invariant stdNonRebasingBalanceEqCreditBalances(address account) + OUSD.rebaseState[account] == StdNonRebasing() => OUSD.balanceOf(account) == OUSD.creditBalances[account] + { + preserved { + requireInvariant yieldToOfZeroIsZero(); + requireInvariant yieldFromOfZeroIsZero(); + requireInvariant DelegationAccountsCorrelation(); + requireInvariant DelegationValidRebaseState(); + requireInvariant nonZeroAlternativeCreditsPerTokenStates(); + requireInvariant stdNonRebasingDoesntYield(); + requireInvariant alternativeCreditsPerTokenIsOneOrZeroOnly(); + } + } + +/// @title Any account in StdNonRebasing state doesn't yield to no account. +/// @property Account Invariants +invariant stdNonRebasingDoesntYield() + forall address account . OUSD.rebaseState[account] == StdNonRebasing() => OUSD.yieldTo[account] == 0 + { + preserved { + requireInvariant yieldToOfZeroIsZero(); + requireInvariant yieldFromOfZeroIsZero(); + requireInvariant DelegationAccountsCorrelation(); + requireInvariant DelegationValidRebaseState(); + } + } + +/// @title alternativeCreditsPerToken can only be set to 0 or 1e18, no other values +/// @property Account Invariants +invariant alternativeCreditsPerTokenIsOneOrZeroOnly() + forall address account . OUSD.alternativeCreditsPerToken[account] == 0 || OUSD.alternativeCreditsPerToken[account] == e18(); + +/// @title Any account with rebaseState = YieldDelegationSource has a nonZero yieldTo +/// @property Account Invariants +invariant yieldDelegationSourceHasNonZeroYeildTo() + forall address account . OUSD.rebaseState[account] == YieldDelegationSource() => OUSD.yieldTo[account] != 0; + +/// @title Any account with rebaseState = YieldDelegationTarget has a nonZero yieldFrom +/// @property Account Invariants +invariant yieldDelegationTargetHasNonZeroYeildFrom() + forall address account . OUSD.rebaseState[account] == YieldDelegationTarget() => OUSD.yieldFrom[account] != 0; + +// Helper Invariants +/// @title yieldTo of zero is zero +/// @property Account Invariants +invariant yieldToOfZeroIsZero() + yieldTo(0) == 0; + +/// @title yieldFrom of zero is zero +/// @property Account Invariants +invariant yieldFromOfZeroIsZero() + yieldFrom(0) == 0; + +/// @title yieldTo of an account can't be the same as the account +/// @property Account Invariants +invariant cantYieldToSelf() + forall address account . OUSD.yieldTo[account] != 0 => OUSD.yieldTo[account] != account; + +/// @title yieldFrom of an account can't be the same as the account +/// @property Account Invariants +invariant cantYieldFromSelf() + forall address account . OUSD.yieldFrom[account] != 0 => OUSD.yieldFrom[account] != account; + +/// @title Only delegation changes the different effective identity. +/// @property Account Invariants +rule onlyDelegationChangesPairingState(address accountA, address accountB, method f) +filtered{f -> !f.isView} +{ + bool different_before = differentAccounts(accountA, accountB); + env e; + calldataarg args; + f(e, args); + bool different_after = differentAccounts(accountA, accountB); + + if(delegateMethods(f)) { + satisfy different_before != different_after; + } + assert different_before != different_after => delegateMethods(f); +} diff --git a/certora/specs/OUSD/BalanceInvariants.spec b/certora/specs/OUSD/BalanceInvariants.spec new file mode 100644 index 0000000000..56f25375da --- /dev/null +++ b/certora/specs/OUSD/BalanceInvariants.spec @@ -0,0 +1,183 @@ +import "./common.spec"; +import "./AccountInvariants.spec"; + +// holds the credits sum of all accounts in stdNonRebasing state. +ghost mathint sumAllNonRebasingBalances { + init_state axiom sumAllNonRebasingBalances == 0; +} + +// holds the credits sum of all accounts in one of the following states: NotSet, StdRebasing, and YieldDelegationTarget. +ghost mathint sumAllRebasingBalances { + init_state axiom sumAllRebasingBalances == 0; +} + +ghost mapping(address => uint256) creditBalancesMirror { + init_state axiom forall address account . creditBalancesMirror[account] == 0; +} + +ghost mapping(address => OUSD.RebaseOptions) rebaseStateMirror { + init_state axiom forall address account . rebaseStateMirror[account] == NotSet(); +} + +hook Sload uint256 creditBalance creditBalances[KEY address account] { + require creditBalance == creditBalancesMirror[account]; +} + +hook Sload OUSD.RebaseOptions rebaseOption rebaseState[KEY address account] { + require rebaseOption == rebaseStateMirror[account]; +} + +// rebaseState is always updated before updateing the credits balance (when it is updated) so the best way to keep track of credit balance per state +// is to add the changes when the credits are updated with consideration to the current account rebasing state. +hook Sstore creditBalances[KEY address account] uint256 new_creditBalance (uint256 old_creditBalance) { + require old_creditBalance == creditBalancesMirror[account]; + if (rebaseStateMirror[account] == StdNonRebasing()) { + sumAllNonRebasingBalances = sumAllNonRebasingBalances - old_creditBalance + new_creditBalance; + } else if (rebaseStateMirror[account] != YieldDelegationSource()) { + sumAllRebasingBalances = sumAllRebasingBalances - old_creditBalance + new_creditBalance; + } + creditBalancesMirror[account] = new_creditBalance; +} + +hook Sstore rebaseState[KEY address account] OUSD.RebaseOptions new_rebaseOption (OUSD.RebaseOptions old_rebaseOption) { + require old_rebaseOption == rebaseStateMirror[account]; + // transitioning out of StdNonRebasing state - substract balance from sumAllNonRebasing + if(old_rebaseOption == StdNonRebasing() && new_rebaseOption != StdNonRebasing()) { + sumAllNonRebasingBalances = sumAllNonRebasingBalances - creditBalancesMirror[account]; + // transitioning into StdNonRebasing state - add balance to sumAllNonRebasing + } else if (old_rebaseOption != StdNonRebasing() && new_rebaseOption == StdNonRebasing()) { + sumAllNonRebasingBalances = sumAllNonRebasingBalances + creditBalancesMirror[account]; + } + // transitioning into rebasing state - add balance to sumAllRebasing + if (!isRebasing(old_rebaseOption) && isRebasing(new_rebaseOption)) { + sumAllRebasingBalances = sumAllRebasingBalances + creditBalancesMirror[account]; + } + // transitioning out of rebasing state - subtract balance from sumAllRebasing + else if (isRebasing(old_rebaseOption) && !isRebasing(new_rebaseOption)) { + sumAllRebasingBalances = sumAllRebasingBalances - creditBalancesMirror[account]; + } + + rebaseStateMirror[account] = new_rebaseOption; +} + +/// @title The sum of all RebaseOptions.StdNonRebasing accounts equals the nonRebasingSupply. +/// @property Balance Invariants +invariant sumAllNonRebasingBalancesEqNonRebasingSupply() + sumAllNonRebasingBalances == nonRebasingSupply() + { + preserved { + initTotalSupply(); + allAccountValidState(); + require OUSD.rebasingCreditsPerToken_ >= e18(); + } + } + +/// @title The sum of the credits in all NotSet, StdRebasing, and YieldDelegationTarget accounts equal the rebasingCredits. +/// @property Balance Invariants +invariant sumAllRebasingCreditsEqRebasingCredits() + sumAllRebasingBalances == rebasingCreditsHighres() + { + preserved { + initTotalSupply(); + allAccountValidState(); + require OUSD.rebasingCreditsPerToken_ >= e18(); + } + } + +/// @title Ensure correlation between the sum of the credits in all NotSet, StdRebasing, and YieldDelegationTarget accounts match +/// the rebasingCredits allowing for a bounded rounding error calculated as `rebasingCreditsPerToken / 1e18` for both rebaseOptIn and governanceRebaseOptIn. +/// @property Balance Invariants +rule sumAllRebasingCreditsAndTotalRebasingCreditsCorelation(method f) + filtered{f -> f.selector == sig:rebaseOptIn().selector || + f.selector == sig:governanceRebaseOptIn(address).selector} + { + env e; + calldataarg args; + + initTotalSupply(); + allAccountValidState(); + require OUSD.rebasingCreditsPerToken_ >= e18(); + + require sumAllRebasingBalances == rebasingCreditsHighres(); + + f(e, args); + + assert EqualUpTo(sumAllRebasingBalances, rebasingCreditsHighres(), BALANCE_ROUNDING_ERROR(OUSD.rebasingCreditsPerToken_)); +} + +/// @title Verify that the total supply remains within the maximum allowable limit. +/// @property Balance Invariants +invariant totalSupplyLessThanMaxSupply() + OUSD.totalSupply() <= max_uint128 + { + preserved { + initTotalSupply(); + allAccountValidState(); + require OUSD.rebasingCreditsPerToken_ >= e18(); + requireInvariant sumAllNonRebasingBalancesEqNonRebasingSupply(); + require isInitialized(); + } + } + +/// @title Verify that the total balance of delegator and delegatee remains unchanged after yield delegation. +/// @property Balance Invariants +rule delegateYieldPreservesSumOfBalances(address from, address to, address other) { + env e; + initTotalSupply(); + allAccountValidState(); + require OUSD.rebasingCreditsPerToken_ >= e18(); + require other != from; + require other != to; + + uint256 fromBalancePre = OUSD.balanceOf(from); + uint256 toBalancePre = OUSD.balanceOf(to); + uint256 otherBalancePre = OUSD.balanceOf(other); + uint256 totalSupplyPre = OUSD.totalSupply(); + + mathint sumBalancesPre = fromBalancePre + toBalancePre; + + delegateYield(e, from, to); + + uint256 fromBalancePost = OUSD.balanceOf(from); + uint256 toBalancePost = OUSD.balanceOf(to); + uint256 otherBalancePost = OUSD.balanceOf(other); + uint256 totalSupplyPost = OUSD.totalSupply(); + + mathint sumBalancesPost = fromBalancePost + toBalancePost; + + assert sumBalancesPre == sumBalancesPost; + assert otherBalancePre == otherBalancePost; + assert totalSupplyPre == totalSupplyPost; +} + +/// @title Verify that the total balance of delegator and delegatee remains unchanged after undelegation. +/// @property Balance Invariants +rule undelegateYieldPreservesSumOfBalances(address from, address other) { + env e; + initTotalSupply(); + allAccountValidState(); + require OUSD.rebasingCreditsPerToken_ >= e18(); + address yieldedTo = OUSD.yieldTo[from]; + require other != from; + require other != yieldedTo; + + uint256 fromBalancePre = OUSD.balanceOf(from); + uint256 toBalancePre = OUSD.balanceOf(yieldedTo); + uint256 otherBalancePre = OUSD.balanceOf(other); + uint256 totalSupplyPre = OUSD.totalSupply(); + + mathint sunBalancesPre = fromBalancePre + toBalancePre; + + undelegateYield(e, from); + + uint256 fromBalancePost = OUSD.balanceOf(from); + uint256 toBalancePost = OUSD.balanceOf(yieldedTo); + uint256 otherBalancePost = OUSD.balanceOf(other); + uint256 totalSupplyPost = OUSD.totalSupply(); + + mathint sunBalancesPost = fromBalancePost + toBalancePost; + + assert sunBalancesPost == sunBalancesPre; + assert otherBalancePre == otherBalancePost; + assert totalSupplyPre == totalSupplyPost; +} diff --git a/certora/specs/OUSD/OtherInvariants.spec b/certora/specs/OUSD/OtherInvariants.spec new file mode 100644 index 0000000000..de3ebdaf11 --- /dev/null +++ b/certora/specs/OUSD/OtherInvariants.spec @@ -0,0 +1,312 @@ +import "./common.spec"; +import "./AccountInvariants.spec"; +import "./BalanceInvariants.spec"; + +use invariant sumAllNonRebasingBalancesEqNonRebasingSupply; +use invariant sumAllRebasingCreditsEqRebasingCredits; + +/// @title Verify account balance integrity based on rebase state +// Ensures balances are correctly calculated for Yield Delegation Targets, Standard Rebasing, +// Non-Rebasing, and undefined (NotSet) states to maintain consistency in OUSD accounting. +/// @property Balance Integrities +rule balanceOfIntegrity(address account) { + env e; + initTotalSupply(); + allAccountValidState(); + require OUSD.rebasingCreditsPerToken_ >= e18(); + + OUSD.RebaseOptions accountState = OUSD.rebaseState[account]; + + address delegator = OUSD.yieldFrom[account]; + uint256 balance = balanceOf(account); + uint256 delegatorBalance = balanceOf(delegator); + + mathint baseBalance = (OUSD.creditBalances[account] * e18()) / OUSD.rebasingCreditsPerToken_; + + if (accountState == YieldDelegationTarget()) { + assert balance + delegatorBalance == baseBalance; + } else if (accountState == NotSet() || accountState == StdRebasing()) { + assert balance == baseBalance; + } else if (accountState == StdNonRebasing()) { + assert balance == OUSD.creditBalances[account]; + } + assert true; +} + +/// @title After a non-reverting call to rebaseOptIn() the alternativeCreditsPerToken[account] == 0 and does not result in a change in account balance. +/// @property Balance Integrities +rule rebaseOptInIntegrity() { + env e; + address account = e.msg.sender; + initTotalSupply(); + allAccountValidState(); + require OUSD.rebasingCreditsPerToken_ >= e18(); + + uint256 balancePre = OUSD.balanceOf(account); + + OUSD.rebaseOptIn(e); + + uint256 balancePost = OUSD.balanceOf(account); + + assert OUSD.alternativeCreditsPerToken[account] == 0; + assert balancePre == balancePost; +} + +/// @title After a non-reverting call to governanceRebaseOptIn() the alternativeCreditsPerToken[account] == 0 and does not result in a change in account balance. +/// @property Balance Integrities +rule governanceRebaseOptInIntegrity(address account) { + env e; + initTotalSupply(); + allAccountValidState(); + require OUSD.rebasingCreditsPerToken_ >= e18(); + + uint256 balancePre = OUSD.balanceOf(account); + + OUSD.governanceRebaseOptIn(e, account); + + uint256 balancePost = OUSD.balanceOf(account); + + assert OUSD.alternativeCreditsPerToken[account] == 0; + assert balancePre == balancePost; +} + +/// @title After a non-reverting call to rebaseOptOut() the alternativeCreditsPerToken[account] == 1e18 and does not result in a change in account balance. +/// @property Balance Integrities +rule rebaseOptOutIntegrity() { + env e; + address account = e.msg.sender; + initTotalSupply(); + allAccountValidState(); + + uint256 balancePre = OUSD.balanceOf(account); + + OUSD.rebaseOptOut(e); + + uint256 balancePost = OUSD.balanceOf(account); + + assert OUSD.alternativeCreditsPerToken[account] == e18(); + assert balancePre == balancePost; +} + +/// @title Only transfer, transferFrom, mint, burn, and changeSupply result in a change in any account's balance. +/// @property Balance Integrities +rule whoCanChangeBalance(method f, address account) { + env e; + calldataarg args; + initTotalSupply(); + allAccountValidState(); + require OUSD.rebasingCreditsPerToken_ >= e18(); + require isInitialized(); + + uint256 balancePre = OUSD.balanceOf(account); + + f(e, args); + + uint256 balancePost = OUSD.balanceOf(account); + + assert balancePre != balancePost => whoCanChangeBalance(f); +} + +/// @title A successful mint() call by the vault results in the target account's balance increasing by the amount specified. +/// @property Balance Integrities +rule mintIntegrity(address account, uint256 amount) { + env e; + initTotalSupply(); + allAccountValidState(); + require OUSD.rebasingCreditsPerToken_ >= e18(); + + uint256 balancePre = OUSD.balanceOf(account); + + mint(e, account, amount); + + uint256 balancePost = OUSD.balanceOf(account); + + assert balancePost == balancePre + amount; +} + +/// @title A successful burn() call by the vault results in the target account's balance decreasing by the amount specified. +/// @property Balance Integrities +rule burnIntegrity(address account, uint256 amount) { + env e; + initTotalSupply(); + allAccountValidState(); + require OUSD.rebasingCreditsPerToken_ >= e18(); + + uint256 balancePre = OUSD.balanceOf(account); + + burn(e, account, amount); + + uint256 balancePost = OUSD.balanceOf(account); + + assert balancePost == balancePre - amount; +} + +/// @title After a call to changeSupply() then nonRebasingCredits + (rebasingCredits / rebasingCreditsPer) <= totalSupply and the new totalSupply match what was passed into the call. +/// @property Balance Integrities +rule changeSupplyIntegrity(uint256 newTotalSupply) { + env e; + initTotalSupply(); + allAccountValidState(); + requireInvariant sumAllNonRebasingBalancesEqNonRebasingSupply(); + requireInvariant sumAllRebasingCreditsEqRebasingCredits(); + require OUSD.rebasingCreditsPerToken_ >= e18(); + require newTotalSupply >= OUSD.totalSupply(); + + OUSD.changeSupply(e, newTotalSupply); + + assert OUSD.totalSupply() == newTotalSupply; + assert (nonRebasingSupply() + (rebasingCreditsHighres() / OUSD.rebasingCreditsPerToken_)) <= OUSD.totalSupply(); +} + +/// @title Only transfers, mints, and burns change the balance of StdNonRebasing and YieldDelegationSource accounts. +/// @property Balance Integrities +rule whoCanChangeNonRebasingBalance(method f, address account) { + env e; + calldataarg args; + initTotalSupply(); + allAccountValidState(); + require OUSD.rebasingCreditsPerToken_ >= e18(); + require isInitialized(); + + uint256 balancePre = OUSD.balanceOf(account); + + f(e, args); + + uint256 balancePost = OUSD.balanceOf(account); + OUSD.RebaseOptions statePost = OUSD.rebaseState[account]; + + assert balancePre != balancePost && (statePost == StdNonRebasing() || statePost == YieldDelegationSource()) => + f.selector == sig:transfer(address, uint256) .selector || + f.selector == sig:transferFrom(address, address, uint256).selector || + f.selector == sig:mint(address, uint256).selector || + f.selector == sig:burn(address, uint256).selector; +} + +/// @title Recipient and sender (msg.sender) account balances should increase and decrease respectively by the amount after a transfer operation +// Account balance should not change after a transfer operation if the recipient is the sender. +/// @property Balance Integrities +rule transferIntegrityTo(address account, uint256 amount) { + env e; + require sufficientResolution(); + allAccountValidState(); + initTotalSupply(); + + uint256 toBalanceBefore = balanceOf(account); + uint256 fromBalanceBefore = balanceOf(e.msg.sender); + + transfer(e, account, amount); + + uint256 toBalanceAfter = balanceOf(account); + uint256 fromBalanceAfter = balanceOf(e.msg.sender); + + assert account != e.msg.sender => toBalanceAfter - toBalanceBefore == amount; + assert account != e.msg.sender => fromBalanceBefore - fromBalanceAfter == amount; + assert account == e.msg.sender => toBalanceBefore == toBalanceAfter; +} + +/// @title Transfer doesn't change the balance of a third party. +/// @property Balance Integrities +rule transferThirdParty(address account) { + env e; + address to; + uint256 amount; + + uint256 otherBalanceBefore = balanceOf(account); + require sufficientResolution(); + + // requiring proved invariants + allAccountValidState(); + initTotalSupply(); + + transfer(e, to, amount); + uint256 otherUserBalanceAfter = balanceOf(account); + + assert (e.msg.sender != account && to != account) => otherBalanceBefore == otherUserBalanceAfter; +} + +/// @title Account balance should be increased by the amount minted. +/// @property Balance Integrities +rule mintIntegrityTo(address account, uint256 amount) { + env e; + uint256 balanceBefore = balanceOf(account); + + // assumption for the known rounding error that goes like ~ 10^18 / rebasingCreditsPerToken_ + require sufficientResolution(); + + // requiring proved invariants + allAccountValidState(); + initTotalSupply(); + + mint(e, account, amount); + + uint256 balanceAfter = balanceOf(account); + + assert balanceAfter - balanceBefore == amount; +} + +/// @title Any third-party account balance should not change after a mint operation. +/// @property Balance Integrities +rule mintIntegrityThirdParty(address account) { + env e; + address to; + uint256 amount; + + uint256 otherBalanceBefore = balanceOf(account); + + // assumption for the known rounding error that goes like ~ 10^18 / rebasingCreditsPerToken_ + require sufficientResolution(); + + // requiring proved invariants + allAccountValidState(); + initTotalSupply(); + + mint(e, to, amount); + + uint256 otherBalanceAfter = balanceOf(account); + + assert to != account => otherBalanceBefore == otherBalanceAfter; +} + +/// @title Account balance should be decreased by the amount burned. +/// @property Balance Integrities +rule burnIntegrityTo(address account, uint256 amount) { + env e; + + uint256 balanceBefore = balanceOf(account); + + // assumption for the known rounding error that goes like ~ 10^18 / rebasingCreditsPerToken_ + require sufficientResolution(); + + // requiring proved invariants + allAccountValidState(); + initTotalSupply(); + + burn(e, account, amount); + + uint256 balanceAfter = balanceOf(account); + + assert balanceBefore - balanceAfter == amount; +} + +/// @title Any third-party account balance should not change after a burn operation. +/// @property Balance Integrities +rule burnIntegrityThirdParty(address account) { + env e; + address to; + uint256 amount; + + uint256 otherBalanceBefore = balanceOf(account); + + // assumption for the known rounding error that goes like ~ 10^18 / rebasingCreditsPerToken_ + require sufficientResolution(); + + // requiring proved invariants + allAccountValidState(); + initTotalSupply(); + + burn(e, to, amount); + + uint256 otherBalanceAfter = balanceOf(account); + + assert to != account => otherBalanceBefore == otherBalanceAfter; +} diff --git a/certora/specs/OUSD/SumOfBalances.spec b/certora/specs/OUSD/SumOfBalances.spec new file mode 100644 index 0000000000..cf08a24b63 --- /dev/null +++ b/certora/specs/OUSD/SumOfBalances.spec @@ -0,0 +1,195 @@ +import "BalanceInvariants.spec"; + +definition whoChangesMultipleBalances(method f) returns bool = + f.selector == sig:initialize(address,uint256).selector || + f.selector == sig:changeSupply(uint256).selector; + +definition whoChangesSingleBalance(method f) returns bool = + !delegateMethods(f) && + !transferMethods(f) && + !whoChangesMultipleBalances(f); + +/// This function is symmetric with respect to exchange of user <-> yieldFrom[user] and user <-> yieldTo[user]. +function effectiveBalance(address user) returns mathint { + if (rebaseState(user) == YieldDelegationTarget()) { + return balanceOf(user) + balanceOf(OUSD.yieldFrom[user]); + } else if (rebaseState(user) == YieldDelegationSource()) { + return balanceOf(user) + balanceOf(OUSD.yieldTo[user]); + } else { + return to_mathint(balanceOf(user)); + } +} + +/// @title Auxiliary rule: the effective balance is the same for the same effective account. +rule effectiveBalanceIsEquivalentForSameAccounts(address accountA, address accountB) { + require OUSD.rebasingCreditsPerToken_ >= e18(); + allAccountValidState(); + + assert !differentAccounts(accountA, accountB) => effectiveBalance(accountA) == effectiveBalance(accountB); +} + +/// @title Both transfer methods must preserve the sum of balances. +/// The total supply and any balance of a third party cannot change. +/// @property Balance Invariants +rule transferPreservesSumOfBalances(address accountA, address accountB, method f) +filtered{f -> transferMethods(f)} +{ + //require OUSD.rebasingCreditsPerToken_ >= e18(); + /// Under-approximation : otherwise timesout. + require OUSD.rebasingCreditsPerToken_ == 1001*e18()/1000; + allAccountValidState(); + initTotalSupply(); + /// Third party (different user). + address other; + require differentAccounts(other, accountA); + require differentAccounts(other, accountB); + + mathint balanceA_pre = effectiveBalance(accountA); + mathint balanceB_pre = effectiveBalance(accountB); + mathint sumOfBalances_pre = differentAccounts(accountA, accountB) ? balanceA_pre + balanceB_pre : balanceA_pre; + mathint balanceO_pre = effectiveBalance(other); + mathint totalSupply_pre = totalSupply(); + env e; + if(f.selector == sig:transfer(address,uint256).selector) { + require e.msg.sender == accountA; + transfer(e, accountB, _); + } else if(f.selector == sig:transferFrom(address,address,uint256).selector) { + transferFrom(e, accountA, accountB, _); + } else { + assert false; + } + mathint balanceA_post = effectiveBalance(accountA); + mathint balanceB_post = effectiveBalance(accountB); + mathint sumOfBalances_post = differentAccounts(accountA, accountB) ? balanceA_post + balanceB_post : balanceA_post; + mathint balanceO_post = effectiveBalance(other); + mathint totalSupply_post = totalSupply(); + + assert differentAccounts(other, accountA), "The third party cannot change its identity status after transfer"; + assert differentAccounts(other, accountB), "The third party cannot change its identity status after transfer"; + assert sumOfBalances_pre == sumOfBalances_post, "The sum of balances must be conserved"; + assert balanceO_pre == balanceO_post, "The balance of a third party cannot change after transfer"; + assert totalSupply_pre == totalSupply_post, "The total supply is invariant to transfer operations"; +} + +/// @title The sum of balances of any two accounts cannot surpass the total supply. +/// @property Balance Invariants +rule sumOfTwoAccountsBalancesLETotalSupply(address accountA, address accountB, method f) +filtered{f -> !f.isView && !whoChangesMultipleBalances(f) && !delegateMethods(f) && !transferMethods(f)} +{ + require OUSD.rebasingCreditsPerToken_ >= e18(); + allAccountValidState(); + initTotalSupply(); + + address other; + require threeDifferentAccounts(accountA, accountB, other); + + mathint balanceA_pre = effectiveBalance(accountA); + mathint balanceB_pre = effectiveBalance(accountB); + mathint sumOfBalances_pre = balanceA_pre + balanceB_pre; + mathint totalSupply_pre = totalSupply(); + env e; + calldataarg args; + f(e, args); + mathint balanceA_post = effectiveBalance(accountA); + mathint balanceB_post = effectiveBalance(accountB); + mathint sumOfBalances_post = balanceA_post + balanceB_post; + mathint totalSupply_post = totalSupply(); + + /// Assume that only accountA and accountB balances change. + require balanceA_pre != balanceA_post && balanceB_pre != balanceB_post; + + assert sumOfBalances_pre <= totalSupply_pre => sumOfBalances_post <= totalSupply_post; +} + +/// @title The sum of all rebasing account balances cannot surpass the total supply after calling for changeSupply. +/// @property Balance Invariants +rule changeSupplyPreservesSumOFRebasingLesEqTotalSupply(uint256 amount) { + env e; + require OUSD.rebasingCreditsPerToken_ >= e18(); + allAccountValidState(); + initTotalSupply(); + require amount >= totalSupply(); + requireInvariant sumAllNonRebasingBalancesEqNonRebasingSupply(); + requireInvariant sumAllRebasingCreditsEqRebasingCredits(); + requireInvariant totalSupplyLessThanMaxSupply(); + + require sumAllRebasingBalances * e18() / OUSD.rebasingCreditsPerToken_ <= totalSupply(); + + changeSupply(e, amount); + + assert sumAllRebasingBalances * e18() / OUSD.rebasingCreditsPerToken_ <= totalSupply(); +} + +/// @title Which methods change the balance of a single account. +rule onlySingleAccountBalanceChange(address accountA, address accountB, method f) +filtered{f -> !f.isView && whoChangesSingleBalance(f)} +{ + require OUSD.rebasingCreditsPerToken_ >= e18(); + allAccountValidState(); + initTotalSupply(); + /// Require different accounts before + require differentAccounts(accountA, accountB); + /// Probe balances before + mathint balanceA_pre = effectiveBalance(accountA); + mathint balanceB_pre = effectiveBalance(accountB); + /// Call an arbitrary function. + env e; + calldataarg args; + f(e, args); + /// Require different accounts after + require differentAccounts(accountA, accountB); + /// Probe balances after + mathint balanceA_post = effectiveBalance(accountA); + mathint balanceB_post = effectiveBalance(accountB); + + assert balanceA_pre != balanceA_post => balanceB_pre == balanceB_post; +} + +/// @title Which methods change the balance of only two accounts. +rule onlyTwoAccountsBalancesChange(address accountA, address accountB, address accountC, method f) +filtered{f -> !f.isView && !whoChangesMultipleBalances(f) && !delegateMethods(f)} +{ + require OUSD.rebasingCreditsPerToken_ >= e18(); + allAccountValidState(); + initTotalSupply(); + /// Require different accounts before + require threeDifferentAccounts(accountA, accountB, accountC); + /// Probe balances before + mathint balanceA_pre = effectiveBalance(accountA); + mathint balanceB_pre = effectiveBalance(accountB); + mathint balanceC_pre = effectiveBalance(accountC); + /// Call an arbitrary function. + env e; + calldataarg args; + f(e, args); + /// Probe balances after + mathint balanceA_post = effectiveBalance(accountA); + mathint balanceB_post = effectiveBalance(accountB); + mathint balanceC_post = effectiveBalance(accountC); + + /// Assert different accounts after + assert threeDifferentAccounts(accountA, accountB, accountC); + assert balanceA_pre != balanceA_post && balanceB_pre != balanceB_post => balanceC_post == balanceC_pre; +} + +/// @title If two accounts become paired / unpaired, then their sum of balances is preserved, and the total supply is unchanged. +rule pairingPreservesSumOfBalances(address accountA, address accountB, method f) +filtered{f -> !f.isView} +{ + require OUSD.rebasingCreditsPerToken_ >= e18(); + allAccountValidState(); + initTotalSupply(); + + bool different_before = differentAccounts(accountA, accountB); + mathint sumOfBalances_pre = balanceOf(accountA) + balanceOf(accountB); + mathint totalSupply_pre = totalSupply(); + env e; + calldataarg args; + f(e, args); + bool different_after = differentAccounts(accountA, accountB); + mathint sumOfBalances_post = balanceOf(accountA) + balanceOf(accountB); + mathint totalSupply_post = totalSupply(); + + assert different_before != different_after => sumOfBalances_post == sumOfBalances_pre; + assert different_before != different_after => totalSupply_pre == totalSupply_post; +} diff --git a/certora/specs/OUSD/common.spec b/certora/specs/OUSD/common.spec new file mode 100644 index 0000000000..0a6caa3007 --- /dev/null +++ b/certora/specs/OUSD/common.spec @@ -0,0 +1,92 @@ +using OUSD as OUSD; + +methods { + function OUSD.yieldTo(address) external returns (address) envfree; + function OUSD.yieldFrom(address) external returns (address) envfree; + function OUSD.totalSupply() external returns (uint256) envfree; + function OUSD.rebasingCreditsPerTokenHighres() external returns (uint256) envfree; + function OUSD.rebasingCreditsPerToken() external returns (uint256) envfree; + function OUSD.rebasingCreditsHighres() external returns (uint256) envfree; + function OUSD.rebasingCredits() external returns (uint256) envfree; + function OUSD.balanceOf(address) external returns (uint256) envfree; + function OUSD.creditsBalanceOf(address) external returns (uint256,uint256) envfree; + function OUSD.creditsBalanceOfHighres(address) external returns (uint256,uint256,bool) envfree; + function OUSD.nonRebasingCreditsPerToken(address) external returns (uint256) envfree; + function OUSD.transfer(address,uint256) external returns (bool); + function OUSD.transferFrom(address,address,uint256) external returns (bool); + function OUSD.allowance(address,address) external returns (uint256) envfree; + function OUSD.approve(address,uint256) external returns (bool); + function OUSD.mint(address,uint256) external; + function OUSD.burn(address,uint256) external; + function OUSD.governanceRebaseOptIn(address) external; + function OUSD.rebaseOptIn() external; + function OUSD.rebaseOptOut() external; + function OUSD.changeSupply(uint256) external; + function OUSD.delegateYield(address, address) external; + function OUSD.undelegateYield(address) external; + function rebaseState(address) external returns (OUSD.RebaseOptions) envfree; + function nonRebasingSupply() external returns (uint256) envfree; +} + +definition e18() returns uint256 = 1000000000000000000; // definition for 1e18 + +definition MIN_TOTAL_SUPPLY() returns mathint = 10^16; + +// RebaseOptions state definitions +definition NotSet() returns OUSD.RebaseOptions = OUSD.RebaseOptions.NotSet; +definition StdRebasing() returns OUSD.RebaseOptions = OUSD.RebaseOptions.StdRebasing; +definition StdNonRebasing() returns OUSD.RebaseOptions = OUSD.RebaseOptions.StdNonRebasing; +definition YieldDelegationTarget() returns OUSD.RebaseOptions = OUSD.RebaseOptions.YieldDelegationTarget; +definition YieldDelegationSource() returns OUSD.RebaseOptions = OUSD.RebaseOptions.YieldDelegationSource; + +function initTotalSupply() { require totalSupply() >= MIN_TOTAL_SUPPLY(); } + +definition sufficientResolution() returns bool = rebasingCreditsPerToken() >= e18(); + +definition EqualUpTo(mathint A, mathint B, mathint TOL) returns bool = + A > B ? A - B <= TOL : B - A <= TOL; + +definition BALANCES_TOL() returns mathint = 2; + +// p = rebasingCreditsPerToken_ +definition BALANCE_ERROR(uint256 p) returns mathint = e18() / p; + +definition BALANCE_ROUNDING_ERROR(uint256 p) returns mathint = p / e18(); + +definition isInitialized() returns bool = OUSD.vaultAddress != 0; + +definition whoCanChangeBalance(method f) returns bool = + f.selector == sig:transfer(address, uint256).selector || + f.selector == sig:transferFrom(address, address, uint256).selector || + f.selector == sig:mint(address, uint256).selector || + f.selector == sig:burn(address, uint256).selector || + f.selector == sig:changeSupply(uint256).selector || + false; + +definition isRebasing(OUSD.RebaseOptions state) returns bool = + state == NotSet() || + state == StdRebasing() || + state == YieldDelegationTarget() || + false; + +definition differentAccounts(address accountA, address accountB) returns bool = + /// Account have different identities + (accountA != accountB) && + /// The yield target of accountA is not accountB + (OUSD.rebaseState[accountA] == YieldDelegationSource() => OUSD.yieldTo[accountA] != accountB) && + /// The yield source of accountA is not accountB + (OUSD.rebaseState[accountA] == YieldDelegationTarget() => OUSD.yieldFrom[accountA] != accountB); + +definition threeDifferentAccounts(address accountA, address accountB, address accountC) returns bool = + differentAccounts(accountA, accountB) && + differentAccounts(accountA, accountC) && + differentAccounts(accountB, accountC); + +definition transferMethods(method f) returns bool = + f.selector == sig:transfer(address,uint256).selector || + f.selector == sig:transferFrom(address,address,uint256).selector; + +definition delegateMethods(method f) returns bool = + f.selector == sig:undelegateYield(address).selector || + f.selector == sig:delegateYield(address,address).selector; + \ No newline at end of file From 18d4a51822fd30499dcaade204a30184b297bbde Mon Sep 17 00:00:00 2001 From: Daniel Von Fange Date: Tue, 7 Jan 2025 11:16:59 -0500 Subject: [PATCH 110/110] Fix certora spec. Starting with an invalid state would result in an invalid state --- certora/specs/OUSD/OtherInvariants.spec | 5 ++++- certora/specs/OUSD/common.spec | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/certora/specs/OUSD/OtherInvariants.spec b/certora/specs/OUSD/OtherInvariants.spec index de3ebdaf11..165543bb2b 100644 --- a/certora/specs/OUSD/OtherInvariants.spec +++ b/certora/specs/OUSD/OtherInvariants.spec @@ -151,10 +151,13 @@ rule changeSupplyIntegrity(uint256 newTotalSupply) { requireInvariant sumAllRebasingCreditsEqRebasingCredits(); require OUSD.rebasingCreditsPerToken_ >= e18(); require newTotalSupply >= OUSD.totalSupply(); + /// If garbage in, then garbage out + require (nonRebasingSupply() + (rebasingCreditsHighres() / OUSD.rebasingCreditsPerToken_)) <= OUSD.totalSupply(); + require OUSD.totalSupply() < MAX_TOTAL_SUPPLY(); OUSD.changeSupply(e, newTotalSupply); - assert OUSD.totalSupply() == newTotalSupply; + assert newTotalSupply < MAX_TOTAL_SUPPLY() ? OUSD.totalSupply() == newTotalSupply : OUSD.totalSupply == MAX_TOTAL_SUPPLY(); assert (nonRebasingSupply() + (rebasingCreditsHighres() / OUSD.rebasingCreditsPerToken_)) <= OUSD.totalSupply(); } diff --git a/certora/specs/OUSD/common.spec b/certora/specs/OUSD/common.spec index 0a6caa3007..ea80a136d4 100644 --- a/certora/specs/OUSD/common.spec +++ b/certora/specs/OUSD/common.spec @@ -31,6 +31,7 @@ methods { definition e18() returns uint256 = 1000000000000000000; // definition for 1e18 definition MIN_TOTAL_SUPPLY() returns mathint = 10^16; +definition MAX_TOTAL_SUPPLY() returns mathint = 2^128 - 1; // RebaseOptions state definitions definition NotSet() returns OUSD.RebaseOptions = OUSD.RebaseOptions.NotSet;