From 92612a25ea11e14786f3df06944706861e5c7080 Mon Sep 17 00:00:00 2001 From: Daniel Von Fange Date: Mon, 6 Feb 2023 08:23:49 -0500 Subject: [PATCH 01/10] Added comments to warn us about doing a split accounting update --- contracts/contracts/token/OUSD.sol | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index bd64a35612..dfbc41a122 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -473,10 +473,16 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { // high resolution, and do not have to do any other bookkeeping nonRebasingCreditsPerToken[_account] = 1e27; } else { - // Migrate an existing account: - + // Migrate the existing account: + // It is important that balanceOf not be called inside updating + // account data, since it will give wrong answers if it does + // not have all an account's data in a consistent state. This + // isn't a problem in the current implimentation, since we only + // need to update nonRebasingCreditsPerToken. // Set fixed credits per token for this account nonRebasingCreditsPerToken[_account] = _rebasingCreditsPerToken; + + // Update global totals: // Update non rebasing supply nonRebasingSupply = nonRebasingSupply.add(balanceOf(_account)); // Update credit tallies From 838d6a799af552a90b674e53bed98e9255899c9a Mon Sep 17 00:00:00 2001 From: Daniel Von Fange Date: Mon, 6 Feb 2023 08:55:27 -0500 Subject: [PATCH 02/10] Reorder and clarify rebaseOptOut and rebaseOptIn --- contracts/contracts/token/OUSD.sol | 33 ++++++++++++++++++------------ 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index dfbc41a122..a3ff8f19d6 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -501,24 +501,29 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { function rebaseOptIn() public nonReentrant { require(_isNonRebasingAccount(msg.sender), "Account has not opted out"); + // Precalculate new credits, so that we avoid internal calls when + // atomicly updating account. // Convert balance into the same amount at the current exchange rate uint256 newCreditBalance = _creditBalances[msg.sender] .mul(_rebasingCreditsPerToken) .div(_creditsPerToken(msg.sender)); - // Decreasing non rebasing supply - nonRebasingSupply = nonRebasingSupply.sub(balanceOf(msg.sender)); - + // Atomicly update this account: + // Important that no internal calls happen during this. + // Remove pinned fixed credits per token + delete nonRebasingCreditsPerToken[msg.sender]; + // New credits _creditBalances[msg.sender] = newCreditBalance; + // Mark explicitly opted out of rebasing + rebaseState[msg.sender] = RebaseOptions.OptIn; + + // Update global totals: + // Decrease non rebasing supply + nonRebasingSupply = nonRebasingSupply.sub(balanceOf(msg.sender)); // Increase rebasing credits, totalSupply remains unchanged so no // adjustment necessary _rebasingCredits = _rebasingCredits.add(_creditBalances[msg.sender]); - - rebaseState[msg.sender] = RebaseOptions.OptIn; - - // Delete any fixed credits per token - delete nonRebasingCreditsPerToken[msg.sender]; } /** @@ -527,17 +532,19 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { function rebaseOptOut() public nonReentrant { require(!_isNonRebasingAccount(msg.sender), "Account has not opted in"); - // Increase non rebasing supply - nonRebasingSupply = nonRebasingSupply.add(balanceOf(msg.sender)); + // Atomicly update this account + // Important that no internal calls happen during this. // Set fixed credits per token nonRebasingCreditsPerToken[msg.sender] = _rebasingCreditsPerToken; + // Mark explicitly opted out of rebasing + rebaseState[msg.sender] = RebaseOptions.OptOut; + // Update global totals: + // Increase non rebasing supply + nonRebasingSupply = nonRebasingSupply.add(balanceOf(msg.sender)); // 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; } /** From f5a55a30c7728c51ad61b8fb79e88ee444d723ea Mon Sep 17 00:00:00 2001 From: Daniel Von Fange Date: Thu, 16 Feb 2023 10:13:49 -0500 Subject: [PATCH 03/10] Use perfect rebasing --- contracts/contracts/token/OUSD.sol | 68 +++++++++++++----------------- 1 file changed, 30 insertions(+), 38 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index a3ff8f19d6..429649a0ec 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -457,7 +457,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) { - _ensureRebasingMigration(_account); + _ensureMigrationToNonRebasing(_account); } return nonRebasingCreditsPerToken[_account] > 0; } @@ -466,30 +466,33 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { * @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) { - 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 the existing account: - // It is important that balanceOf not be called inside updating - // account data, since it will give wrong answers if it does - // not have all an account's data in a consistent state. This - // isn't a problem in the current implimentation, since we only - // need to update nonRebasingCreditsPerToken. - // Set fixed credits per token for this account - nonRebasingCreditsPerToken[_account] = _rebasingCreditsPerToken; - - // Update global totals: - // Update non rebasing supply - nonRebasingSupply = nonRebasingSupply.add(balanceOf(_account)); - // Update credit tallies - _rebasingCredits = _rebasingCredits.sub( - _creditBalances[_account] - ); - } + function _ensureMigrationToNonRebasing(address _account) internal { + if (nonRebasingCreditsPerToken[_account] != 0) { + return; // Account already is non-rebasing + } + 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 { + // Get old values, so we can use them unaffected by changes + uint256 oldBalance = balanceOf(_account); + uint256 oldCredits = _creditBalances[_account]; + + // Atomicly update account information: + // It is important that balanceOf not be called inside updating + // account data, since it will give wrong answers if it does + // not have all an account's data in a consistent state. + nonRebasingCreditsPerToken[_account] = 1e27; + // difference between the 1e18 balance and the new 1e27 resolution + _creditBalances[_account] = oldBalance * 1e9; + + // Verify perfect acccount accounting update + require(oldBalance == balanceOf(_account), "Balances do not match"); + + // Update global totals: + nonRebasingSupply = nonRebasingSupply.add(oldBalance); + _rebasingCredits = _rebasingCredits.sub(oldCredits); } } @@ -516,7 +519,6 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { _creditBalances[msg.sender] = newCreditBalance; // Mark explicitly opted out of rebasing rebaseState[msg.sender] = RebaseOptions.OptIn; - // Update global totals: // Decrease non rebasing supply @@ -532,19 +534,9 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { function rebaseOptOut() public nonReentrant { require(!_isNonRebasingAccount(msg.sender), "Account has not opted in"); - // Atomicly update this account - // Important that no internal calls happen during this. - // Set fixed credits per token - nonRebasingCreditsPerToken[msg.sender] = _rebasingCreditsPerToken; - // Mark explicitly opted out of rebasing - rebaseState[msg.sender] = RebaseOptions.OptOut; + _ensureMigrationToNonRebasing(msg.sender); - // Update global totals: - // Increase non rebasing supply - nonRebasingSupply = nonRebasingSupply.add(balanceOf(msg.sender)); - // Decrease rebasing credits, total supply remains unchanged so no - // adjustment necessary - _rebasingCredits = _rebasingCredits.sub(_creditBalances[msg.sender]); + rebaseState[msg.sender] = RebaseOptions.OptOut; } /** From 1b29fba85e130824084a4b45f912325b67bc61f0 Mon Sep 17 00:00:00 2001 From: Daniel Von Fange Date: Thu, 16 Feb 2023 10:27:03 -0500 Subject: [PATCH 04/10] Switch back to old verison, since it will also be perfectly accurate --- contracts/contracts/token/OUSD.sol | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 429649a0ec..e189e3a009 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -475,23 +475,22 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { // high resolution, and do not have to do any other bookkeeping nonRebasingCreditsPerToken[_account] = 1e27; } else { - // Get old values, so we can use them unaffected by changes - uint256 oldBalance = balanceOf(_account); + // This does not change, but if it did, we want want to + // use the before changes value. uint256 oldCredits = _creditBalances[_account]; // Atomicly update account information: // It is important that balanceOf not be called inside updating // account data, since it will give wrong answers if it does // not have all an account's data in a consistent state. - nonRebasingCreditsPerToken[_account] = 1e27; - // difference between the 1e18 balance and the new 1e27 resolution - _creditBalances[_account] = oldBalance * 1e9; - - // Verify perfect acccount accounting update - require(oldBalance == balanceOf(_account), "Balances do not match"); + // + // By setting a per account nonRebasingCreditsPerToken, + // this account will no longer follow with the global + // rebasing credits per token. + nonRebasingCreditsPerToken[_account] = _rebasingCreditsPerToken; // Update global totals: - nonRebasingSupply = nonRebasingSupply.add(oldBalance); + nonRebasingSupply = nonRebasingSupply.add(balanceOf(_account)); _rebasingCredits = _rebasingCredits.sub(oldCredits); } } From ab9da6ab63a8d574354992d861b608245b7612bb Mon Sep 17 00:00:00 2001 From: Daniel Von Fange Date: Thu, 16 Feb 2023 10:31:55 -0500 Subject: [PATCH 05/10] Follow normal fast exit style, rather than nested ifs --- contracts/contracts/token/OUSD.sol | 37 +++++++++++++++--------------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index e189e3a009..d36c73e696 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -474,25 +474,26 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { // 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 { - // This does not change, but if it did, we want want to - // use the before changes value. - uint256 oldCredits = _creditBalances[_account]; - - // Atomicly update account information: - // It is important that balanceOf not be called inside updating - // account data, since it will give wrong answers if it does - // not have all an account's data in a consistent state. - // - // By setting a per account nonRebasingCreditsPerToken, - // this account will no longer follow with the global - // rebasing credits per token. - nonRebasingCreditsPerToken[_account] = _rebasingCreditsPerToken; - - // Update global totals: - nonRebasingSupply = nonRebasingSupply.add(balanceOf(_account)); - _rebasingCredits = _rebasingCredits.sub(oldCredits); + return; } + + // This does not change, but if it did, we would want to + // use the value before changes. + uint256 oldCredits = _creditBalances[_account]; + + // Atomicly update account information: + // It is important that balanceOf not be called inside updating + // account data, since it will give wrong answers if it does + // not have all an account's data in a consistent state. + // + // By setting a per account nonRebasingCreditsPerToken, + // this account will no longer follow with the global + // rebasing credits per token and will become non-rebasing. + nonRebasingCreditsPerToken[_account] = _rebasingCreditsPerToken; + + // Update global totals + nonRebasingSupply = nonRebasingSupply.add(balanceOf(_account)); + _rebasingCredits = _rebasingCredits.sub(oldCredits); } /** From 958070b76b62c27b94581f9d7075ba7e70a17a16 Mon Sep 17 00:00:00 2001 From: Daniel Von Fange Date: Thu, 16 Feb 2023 10:38:34 -0500 Subject: [PATCH 06/10] Use before change balance, as before --- contracts/contracts/token/OUSD.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index d36c73e696..43da60d618 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -504,6 +504,8 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { function rebaseOptIn() public nonReentrant { require(_isNonRebasingAccount(msg.sender), "Account has not opted out"); + uint256 oldBalance = balanceOf(msg.sender); + // Precalculate new credits, so that we avoid internal calls when // atomicly updating account. // Convert balance into the same amount at the current exchange rate @@ -522,7 +524,7 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { // Update global totals: // Decrease non rebasing supply - nonRebasingSupply = nonRebasingSupply.sub(balanceOf(msg.sender)); + nonRebasingSupply = nonRebasingSupply.sub(oldBalance); // Increase rebasing credits, totalSupply remains unchanged so no // adjustment necessary _rebasingCredits = _rebasingCredits.add(_creditBalances[msg.sender]); From 3ee546110158a2e9d1d8f09821699d18bebad4db Mon Sep 17 00:00:00 2001 From: Daniel Von Fange Date: Wed, 8 Mar 2023 11:47:36 -0500 Subject: [PATCH 07/10] Add events for changes in rebase state --- contracts/contracts/token/OUSD.sol | 64 ++++++++++++++++++++---------- 1 file changed, 42 insertions(+), 22 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 43da60d618..f928a69877 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -31,6 +31,17 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { uint256 rebasingCreditsPerToken ); + event RebasingDisabled( + address indexed account, + uint256 balance, + uint256 rebasingCreditsPerToken + ); + event RebasingEnabled( + address indexed account, + uint256 balance, + uint256 rebasingCreditsPerToken + ); + enum RebaseOptions { NotSet, OptOut, @@ -48,7 +59,7 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { // do not receive yield unless they explicitly opt in) uint256 public nonRebasingSupply; mapping(address => uint256) public nonRebasingCreditsPerToken; - mapping(address => RebaseOptions) public rebaseState; + mapping(address => RebaseOptions) public rebaseState; // User OptIn/OptOut mapping(address => uint256) public isUpgraded; uint256 private constant RESOLUTION_INCREASE = 1e9; @@ -471,29 +482,34 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { return; // Account already is non-rebasing } if (_creditBalances[_account] == 0) { - // Since there is no existing balance, we can directly set to + // Since there is no existing balance, we can directly set it to // high resolution, and do not have to do any other bookkeeping nonRebasingCreditsPerToken[_account] = 1e27; - return; + } else { + // This does not change, but if it did, we would want to + // use the value before changes. + uint256 oldCredits = _creditBalances[_account]; + + // Atomicly update account information: + // It is important that balanceOf not be called inside updating + // account data, since it will give wrong answers if it does + // not have all an account's data in a consistent state. + // + // By setting a per account nonRebasingCreditsPerToken, + // this account will no longer follow with the global + // rebasing credits per token and will become non-rebasing. + nonRebasingCreditsPerToken[_account] = _rebasingCreditsPerToken; + + // Update global totals + nonRebasingSupply = nonRebasingSupply.add(balanceOf(_account)); + _rebasingCredits = _rebasingCredits.sub(oldCredits); } - // This does not change, but if it did, we would want to - // use the value before changes. - uint256 oldCredits = _creditBalances[_account]; - - // Atomicly update account information: - // It is important that balanceOf not be called inside updating - // account data, since it will give wrong answers if it does - // not have all an account's data in a consistent state. - // - // By setting a per account nonRebasingCreditsPerToken, - // this account will no longer follow with the global - // rebasing credits per token and will become non-rebasing. - nonRebasingCreditsPerToken[_account] = _rebasingCreditsPerToken; - - // Update global totals - nonRebasingSupply = nonRebasingSupply.add(balanceOf(_account)); - _rebasingCredits = _rebasingCredits.sub(oldCredits); + emit RebasingDisabled( + _account, + balanceOf(_account), + _rebasingCreditsPerToken + ); } /** @@ -504,10 +520,12 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { function rebaseOptIn() public nonReentrant { require(_isNonRebasingAccount(msg.sender), "Account has not opted out"); + // Precalculate old balance so that no partial + // account changes will affect it uint256 oldBalance = balanceOf(msg.sender); // Precalculate new credits, so that we avoid internal calls when - // atomicly updating account. + // atomically updating account. // Convert balance into the same amount at the current exchange rate uint256 newCreditBalance = _creditBalances[msg.sender] .mul(_rebasingCreditsPerToken) @@ -519,7 +537,7 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { delete nonRebasingCreditsPerToken[msg.sender]; // New credits _creditBalances[msg.sender] = newCreditBalance; - // Mark explicitly opted out of rebasing + // Mark explicitly opted in to rebasing rebaseState[msg.sender] = RebaseOptions.OptIn; // Update global totals: @@ -528,6 +546,8 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { // Increase rebasing credits, totalSupply remains unchanged so no // adjustment necessary _rebasingCredits = _rebasingCredits.add(_creditBalances[msg.sender]); + + emit RebasingEnabled(msg.sender, oldBalance, _rebasingCreditsPerToken); } /** From b8d7a9bb88bd45945090c588372031a9dde7a2cb Mon Sep 17 00:00:00 2001 From: Daniel Von Fange Date: Wed, 8 Mar 2023 11:58:19 -0500 Subject: [PATCH 08/10] More comments updates --- contracts/contracts/token/OUSD.sol | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index f928a69877..03d611ea47 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -501,6 +501,10 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { nonRebasingCreditsPerToken[_account] = _rebasingCreditsPerToken; // Update global totals + // We use the current value of balanceOf, after the update, so + // that if any rounding errors happened in the conversion, we + // will be updating the nonRebasingSupply properly with + // the account balance nonRebasingSupply = nonRebasingSupply.add(balanceOf(_account)); _rebasingCredits = _rebasingCredits.sub(oldCredits); } @@ -541,7 +545,9 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { rebaseState[msg.sender] = RebaseOptions.OptIn; // Update global totals: - // Decrease non rebasing supply + // Decrease non rebasing supply. We use the old balance, since that + // would have been the value that was originally used to adjust the + // nonRebasingSupply. nonRebasingSupply = nonRebasingSupply.sub(oldBalance); // Increase rebasing credits, totalSupply remains unchanged so no // adjustment necessary From 2f63c44009654374ac43c1cf7ec19f08102b064b Mon Sep 17 00:00:00 2001 From: Daniel Von Fange Date: Fri, 10 Mar 2023 11:12:41 -0500 Subject: [PATCH 09/10] Add balance match require to no rebase migration. --- contracts/contracts/token/OUSD.sol | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 03d611ea47..f2e4b6c47c 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -481,6 +481,7 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { if (nonRebasingCreditsPerToken[_account] != 0) { return; // Account already is non-rebasing } + uint256 oldBalance = balanceOf(_account); // For checks if (_creditBalances[_account] == 0) { // Since there is no existing balance, we can directly set it to // high resolution, and do not have to do any other bookkeeping @@ -509,6 +510,10 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { _rebasingCredits = _rebasingCredits.sub(oldCredits); } + // Moving to a non rebasing account should always allow perfect accounting. + // This check does cost extra gas, but migrating accounts is rare. + require(oldBalance == balanceOf(_account), "Balances do not match"); + emit RebasingDisabled( _account, balanceOf(_account), From 063e12a4f1ea0e2a76b92d0da938a3b864db1173 Mon Sep 17 00:00:00 2001 From: Daniel Von Fange Date: Mon, 10 Apr 2023 13:18:42 -0400 Subject: [PATCH 10/10] Add governance opt-in --- contracts/contracts/token/OUSD.sol | 83 ++++++++++++++++++++---------- contracts/test/token/ousd.js | 7 +++ 2 files changed, 62 insertions(+), 28 deletions(-) diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 5356901776..3dfd87534a 100644 --- a/contracts/contracts/token/OUSD.sol +++ b/contracts/contracts/token/OUSD.sol @@ -473,6 +473,44 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { return nonRebasingCreditsPerToken[_account] > 0; } + /** + * @dev Ensures internal account for rebasing and non-rebasing credits and + * supply is updated following deployment of frozen yield change. + */ + function _ensureMigrationToRebasing(address _account) internal { + if (nonRebasingCreditsPerToken[_account] == 0) { + return; // Account already is rebasing + } + // Precalculate old balance so that no partial + // account changes will affect it + uint256 oldBalance = balanceOf(msg.sender); + + // Precalculate new credits, so that we avoid internal calls when + // atomically updating account. + // Convert balance into the same amount at the current exchange rate + uint256 newCreditBalance = _creditBalances[msg.sender] + .mul(_rebasingCreditsPerToken) + .div(_creditsPerToken(msg.sender)); + + // Atomicly update this account: + // Important that no internal calls happen during this. + // Remove pinned fixed credits per token + delete nonRebasingCreditsPerToken[msg.sender]; + // New credits + _creditBalances[msg.sender] = newCreditBalance; + + // Update global totals: + // Decrease non rebasing supply. We use the old balance, since that + // would have been the value that was originally used to adjust the + // nonRebasingSupply. + nonRebasingSupply = nonRebasingSupply.sub(oldBalance); + // Increase rebasing credits, totalSupply remains unchanged so no + // adjustment necessary + _rebasingCredits = _rebasingCredits.add(_creditBalances[msg.sender]); + + emit RebasingEnabled(msg.sender, oldBalance, _rebasingCreditsPerToken); + } + /** * @dev Ensures internal account for rebasing and non-rebasing credits and * supply is updated following deployment of frozen yield change. @@ -529,36 +567,9 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { function rebaseOptIn() public nonReentrant { require(_isNonRebasingAccount(msg.sender), "Account has not opted out"); - // Precalculate old balance so that no partial - // account changes will affect it - uint256 oldBalance = balanceOf(msg.sender); - - // Precalculate new credits, so that we avoid internal calls when - // atomically updating account. - // Convert balance into the same amount at the current exchange rate - uint256 newCreditBalance = _creditBalances[msg.sender] - .mul(_rebasingCreditsPerToken) - .div(_creditsPerToken(msg.sender)); + _ensureMigrationToRebasing(msg.sender); - // Atomicly update this account: - // Important that no internal calls happen during this. - // Remove pinned fixed credits per token - delete nonRebasingCreditsPerToken[msg.sender]; - // New credits - _creditBalances[msg.sender] = newCreditBalance; - // Mark explicitly opted in to rebasing rebaseState[msg.sender] = RebaseOptions.OptIn; - - // Update global totals: - // Decrease non rebasing supply. We use the old balance, since that - // would have been the value that was originally used to adjust the - // nonRebasingSupply. - nonRebasingSupply = nonRebasingSupply.sub(oldBalance); - // Increase rebasing credits, totalSupply remains unchanged so no - // adjustment necessary - _rebasingCredits = _rebasingCredits.add(_creditBalances[msg.sender]); - - emit RebasingEnabled(msg.sender, oldBalance, _rebasingCreditsPerToken); } /** @@ -572,6 +583,22 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { rebaseState[msg.sender] = RebaseOptions.OptOut; } + /** + * @dev Governance action to allow a contract that does not support + * opting in to earn yield + */ + function rebaseOptInByGovernance(address _account) + external + onlyGovernor + nonReentrant + { + require(_isNonRebasingAccount(_account), "Account has not opted out"); + + _ensureMigrationToRebasing(_account); + + rebaseState[_account] = RebaseOptions.OptIn; + } + /** * @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 0030e33549..4bff09eda6 100644 --- a/contracts/test/token/ousd.js +++ b/contracts/test/token/ousd.js @@ -530,6 +530,13 @@ describe("Token", function () { ); }); + it("Should not allow a non governor account to call rebaseOptInByGovernance", async () => { + let { ousd, matt } = await loadFixture(defaultFixture); + await expect( + ousd.connect(matt).rebaseOptInByGovernance(matt.address) + ).to.be.revertedWith("Caller is not the Governor"); + }); + 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 } = await loadFixture( defaultFixture