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..165543bb2b --- /dev/null +++ b/certora/specs/OUSD/OtherInvariants.spec @@ -0,0 +1,315 @@ +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(); + /// If garbage in, then garbage out + require (nonRebasingSupply() + (rebasingCreditsHighres() / OUSD.rebasingCreditsPerToken_)) <= OUSD.totalSupply(); + require OUSD.totalSupply() < MAX_TOTAL_SUPPLY(); + + OUSD.changeSupply(e, newTotalSupply); + + assert newTotalSupply < MAX_TOTAL_SUPPLY() ? OUSD.totalSupply() == newTotalSupply : OUSD.totalSupply == MAX_TOTAL_SUPPLY(); + 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..ea80a136d4 --- /dev/null +++ b/certora/specs/OUSD/common.spec @@ -0,0 +1,93 @@ +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; +definition MAX_TOTAL_SUPPLY() returns mathint = 2^128 - 1; + +// 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 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/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/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/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/flipper/Flipper.sol b/contracts/contracts/flipper/Flipper.sol index 06fc0568d6..11a71bd575 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,10 @@ 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 +66,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 +80,10 @@ 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 +95,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 +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(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 +122,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 +134,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 +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(), 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/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/mocks/TestUpgradedOUSD.sol b/contracts/contracts/mocks/TestUpgradedOUSD.sol new file mode 100644 index 0000000000..5a6eaa7890 --- /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/OETH.sol b/contracts/contracts/token/OETH.sol index 6c6b907b5a..dbd491fbbb 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 pure override returns (string memory) { + return "OETH"; + } + function name() external pure override returns (string memory) { + return "Origin Ether"; + } + + function decimals() external pure override returns (uint8) { + return 18; + } } diff --git a/contracts/contracts/token/OETHBase.sol b/contracts/contracts/token/OETHBase.sol index 1cf3f653ca..9f0c6d1b57 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 pure override returns (string memory) { + return "superOETHb"; + } + + function name() external pure override returns (string memory) { + return "Super OETH"; + } + + function decimals() external pure override returns (uint8) { + return 18; } } diff --git a/contracts/contracts/token/OUSD.sol b/contracts/contracts/token/OUSD.sol index 5f8dba4c46..9c9f87b750 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; /** @@ -7,65 +7,126 @@ 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"; +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 Initializable, InitializableERC20Detailed, Governable { - using SafeMath for uint256; - using StableMath for uint256; +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 { NotSet, - OptOut, - OptIn - } - - 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); - mapping(address => uint256) private _creditBalances; - uint256 private _rebasingCredits; - uint256 private _rebasingCreditsPerToken; - // Frozen address/credits are non rebasing (value is held in contracts which - // do not receive yield unless they explicitly opt in) + StdNonRebasing, + StdRebasing, + YieldDelegationSource, + YieldDelegationTarget + } + + 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_; + /// @dev The amount of tokens that are not rebasing - receiving yield uint256 public nonRebasingSupply; - mapping(address => uint256) public nonRebasingCreditsPerToken; + mapping(address => uint256) internal alternativeCreditsPerToken; + /// @dev A map of all addresses and their respective RebaseOptions mapping(address => RebaseOptions) public rebaseState; - mapping(address => uint256) public isUpgraded; + 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 + { + require(_vaultAddress != address(0), "Zero vault address"); + require(vaultAddress == address(0), "Already initialized"); - function initialize( - string calldata _nameArg, - string calldata _symbolArg, - address _vaultAddress, - uint256 _initialCreditsPerToken - ) external onlyGovernor initializer { - InitializableERC20Detailed._initialize(_nameArg, _symbolArg, 18); - _rebasingCreditsPerToken = _initialCreditsPerToken; + rebasingCreditsPerToken_ = _initialCreditsPerToken; 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; + } + /** * @dev Verifies that the caller is the Vault contract */ @@ -75,66 +136,64 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { } /** - * @return The total supply of OUSD. + * @return High resolution rebasingCreditsPerToken */ - function totalSupply() public view override returns (uint256) { - return _totalSupply; + function rebasingCreditsPerTokenHighres() external view returns (uint256) { + return rebasingCreditsPerToken_; } /** * @return Low resolution rebasingCreditsPerToken */ - function rebasingCreditsPerToken() public view returns (uint256) { - return _rebasingCreditsPerToken / RESOLUTION_INCREASE; + function rebasingCreditsPerToken() external view returns (uint256) { + return rebasingCreditsPerToken_ / RESOLUTION_INCREASE; } /** - * @return Low resolution total number of rebasing credits - */ - function rebasingCredits() public view returns (uint256) { - return _rebasingCredits / RESOLUTION_INCREASE; - } - - /** - * @return High resolution rebasingCreditsPerToken + * @return High resolution total number of rebasing credits */ - function rebasingCreditsPerTokenHighres() public view returns (uint256) { - return _rebasingCreditsPerToken; + function rebasingCreditsHighres() external 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() external view returns (uint256) { + return rebasingCredits_ / RESOLUTION_INCREASE; } /** - * @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. */ - function balanceOf(address _account) - public - view - override - returns (uint256) - { - if (_creditBalances[_account] == 0) return 0; - return - _creditBalances[_account].divPrecisely(_creditsPerToken(_account)); + function balanceOf(address _account) public view returns (uint256) { + RebaseOptions state = rebaseState[_account]; + 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]; + } + 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; } /** - * @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 * address */ function creditsBalanceOf(address _account) - public + external view returns (uint256, uint256) { @@ -143,23 +202,23 @@ contract OUSD is Initializable, InitializableERC20Detailed, 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 ); } } /** - * @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 */ function creditsBalanceOfHighres(address _account) - public + external view returns ( uint256, @@ -168,269 +227,214 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { ) { return ( - _creditBalances[_account], + creditBalances[_account], _creditsPerToken(_account), - isUpgraded[_account] == 1 + true // all accounts have their resolution "upgraded" ); } + // Backwards compatible view + function nonRebasingCreditsPerToken(address _account) + external + view + returns (uint256) + { + return alternativeCreditsPerToken[_account]; + } + /** - * @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. */ - function transfer(address _to, uint256 _value) - public - override - returns (bool) - { + function transfer(address _to, uint256 _value) external returns (bool) { require(_to != address(0), "Transfer to zero address"); - require( - _value <= balanceOf(msg.sender), - "Transfer greater than balance" - ); _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. + * @return true on success. */ function transferFrom( address _from, address _to, uint256 _value - ) public override returns (bool) { + ) external returns (bool) { require(_to != address(0), "Transfer to zero address"); - require(_value <= balanceOf(_from), "Transfer greater than balance"); + uint256 userAllowance = allowances[_from][msg.sender]; + require(_value <= userAllowance, "Allowance exceeded"); - _allowances[_from][msg.sender] = _allowances[_from][msg.sender].sub( - _value - ); + unchecked { + allowances[_from][msg.sender] = userAllowance - _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, uint256 _value ) internal { - bool isNonRebasingTo = _isNonRebasingAccount(_to); - bool isNonRebasingFrom = _isNonRebasingAccount(_from); + ( + int256 fromRebasingCreditsDiff, + int256 fromNonRebasingSupplyDiff + ) = _adjustAccount(_from, -_value.toInt256()); + ( + int256 toRebasingCreditsDiff, + int256 toNonRebasingSupplyDiff + ) = _adjustAccount(_to, _value.toInt256()); + + _adjustGlobals( + fromRebasingCreditsDiff + toRebasingCreditsDiff, + fromNonRebasingSupplyDiff + toNonRebasingSupplyDiff + ); + } - // 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)); + 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) { + revert("Transfer amount exceeds balance"); + } + uint256 newBalance = (currentBalance + _balanceChange).toUint256(); - _creditBalances[_from] = _creditBalances[_from].sub( - creditsDeducted, - "Transfer amount exceeds balance" - ); - _creditBalances[_to] = _creditBalances[_to].add(creditsCredited); - - 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 (state == RebaseOptions.YieldDelegationSource) { + address target = yieldTo[_account]; + uint256 targetOldBalance = balanceOf(target); + uint256 targetNewCredits = _balanceToRebasingCredits( + targetOldBalance + newBalance + ); + rebasingCreditsDiff = + targetNewCredits.toInt256() - + creditBalances[target].toInt256(); + + creditBalances[_account] = newBalance; + creditBalances[target] = targetNewCredits; + } else if (state == RebaseOptions.YieldDelegationTarget) { + uint256 newCredits = _balanceToRebasingCredits( + newBalance + creditBalances[yieldFrom[_account]] + ); + rebasingCreditsDiff = + newCredits.toInt256() - + creditBalances[_account].toInt256(); + creditBalances[_account] = newCredits; + } else { + _autoMigrate(_account); + uint256 alternativeCreditsPerTokenMem = alternativeCreditsPerToken[ + _account + ]; + if (alternativeCreditsPerTokenMem > 0) { + nonRebasingSupplyDiff = _balanceChange; + if (alternativeCreditsPerTokenMem != 1e18) { + alternativeCreditsPerToken[_account] = 1e18; + } + creditBalances[_account] = newBalance; + } else { + uint256 newCredits = _balanceToRebasingCredits(newBalance); + rebasingCreditsDiff = + newCredits.toInt256() - + creditBalances[_account].toInt256(); + creditBalances[_account] = newCredits; + } + } + } + + function _adjustGlobals( + int256 _rebasingCreditsDiff, + int256 _nonRebasingSupplyDiff + ) internal { + if (_rebasingCreditsDiff != 0) { + rebasingCredits_ = (rebasingCredits_.toInt256() + + _rebasingCreditsDiff).toUint256(); + } + if (_nonRebasingSupplyDiff != 0) { + nonRebasingSupply = (nonRebasingSupply.toInt256() + + _nonRebasingSupplyDiff).toUint256(); } } /** - * @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. */ function allowance(address _owner, address _spender) - public + external view - override returns (uint256) { - return _allowances[_owner][_spender]; + return allowances[_owner][_spender]; } /** - * @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. + * @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. + * @return true on success. */ - function approve(address _spender, uint256 _value) - public - override - returns (bool) - { - _allowances[msg.sender][_spender] = _value; + function approve(address _spender, uint256 _value) external returns (bool) { + allowances[msg.sender][_spender] = _value; emit Approval(msg.sender, _spender, _value); 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) - public - returns (bool) - { - _allowances[msg.sender][_spender] = _allowances[msg.sender][_spender] - .add(_addedValue); - emit Approval(msg.sender, _spender, _allowances[msg.sender][_spender]); - 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) - public - returns (bool) - { - uint256 oldValue = _allowances[msg.sender][_spender]; - if (_subtractedValue >= oldValue) { - _allowances[msg.sender][_spender] = 0; - } else { - _allowances[msg.sender][_spender] = oldValue.sub(_subtractedValue); - } - emit Approval(msg.sender, _spender, _allowances[msg.sender][_spender]); - return true; - } - - /** - * @dev Mints new tokens, increasing totalSupply. + * @notice Creates `_amount` tokens and assigns them to `_account`, + * increasing the total supply. */ function mint(address _account, uint256 _amount) external onlyVault { - _mint(_account, _amount); - } - - /** - * @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. - */ - 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)); - _creditBalances[_account] = _creditBalances[_account].add(creditAmount); - - // If the account is non rebasing and doesn't have a set creditsPerToken - // then set it i.e. this is a mint from a fresh contract - if (isNonRebasingAccount) { - nonRebasingSupply = nonRebasingSupply.add(_amount); - } else { - _rebasingCredits = _rebasingCredits.add(creditAmount); - } - - _totalSupply = _totalSupply.add(_amount); - - require(_totalSupply < MAX_SUPPLY, "Max supply"); + // Account + ( + int256 toRebasingCreditsDiff, + int256 toNonRebasingSupplyDiff + ) = _adjustAccount(_account, _amount.toInt256()); + // Globals + _adjustGlobals(toRebasingCreditsDiff, toNonRebasingSupplyDiff); + totalSupply = totalSupply + _amount; + require(totalSupply < MAX_SUPPLY, "Max supply"); emit Transfer(address(0), _account, _amount); } /** - * @dev Burns tokens, decreasing totalSupply. + * @notice Destroys `_amount` tokens from `_account`, + * reducing the total supply. */ - function burn(address account, uint256 amount) external onlyVault { - _burn(account, amount); - } - - /** - * @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. - */ - function _burn(address _account, uint256 _amount) internal nonReentrant { + function burn(address _account, uint256 _amount) external onlyVault { require(_account != address(0), "Burn from the zero address"); if (_amount == 0) { return; } - bool isNonRebasingAccount = _isNonRebasingAccount(_account); - uint256 creditAmount = _amount.mulTruncate(_creditsPerToken(_account)); - 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 - ); - } 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, -_amount.toInt256()); + // Globals + _adjustGlobals(toRebasingCreditsDiff, toNonRebasingSupplyDiff); + totalSupply = totalSupply - _amount; emit Transfer(_account, address(0), _amount); } @@ -445,159 +449,260 @@ contract OUSD is Initializable, InitializableERC20Detailed, Governable { view returns (uint256) { - if (nonRebasingCreditsPerToken[_account] != 0) { - return nonRebasingCreditsPerToken[_account]; + uint256 alternativeCreditsPerTokenMem = alternativeCreditsPerToken[ + _account + ]; + if (alternativeCreditsPerTokenMem != 0) { + return alternativeCreditsPerTokenMem; } else { - return _rebasingCreditsPerToken; + return rebasingCreditsPerToken_; } } /** - * @dev Is an account using rebasing accounting or non-rebasing accounting? - * Also, ensure contracts are non-rebasing if they have not opted in. + * @dev Auto migrate contracts to be non rebasing, + * unless they have opted into yield. * @param _account Address of the account. */ - function _isNonRebasingAccount(address _account) internal returns (bool) { - bool isContract = Address.isContract(_account); - if (isContract && rebaseState[_account] == RebaseOptions.NotSet) { - _ensureRebasingMigration(_account); + function _autoMigrate(address _account) internal { + bool isContract = _account.code.length > 0; + // In previous code versions, contracts would not have had their + // rebaseState[_account] set to RebaseOptions.NonRebasing when migrated + // therefore we check the actual accounting used on the account instead. + if ( + isContract && + rebaseState[_account] == RebaseOptions.NotSet && + alternativeCreditsPerToken[_account] == 0 + ) { + _rebaseOptOut(_account); } - return nonRebasingCreditsPerToken[_account] > 0; } /** - * @dev Ensures internal account for rebasing and non-rebasing credits and - * supply is updated following deployment of frozen yield change. + * @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 Balance of the account. */ - 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 rebasingCredits) + { + // 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; } /** - * @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. + * @notice The calling account will start receiving yield after a successful call. * @param _account Address of the account. */ - function governanceRebaseOptIn(address _account) - public - nonReentrant - onlyGovernor - { + function governanceRebaseOptIn(address _account) external onlyGovernor { + require(_account != address(0), "Zero address not allowed"); _rebaseOptIn(_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. + * @notice The calling account will start receiving yield after a successful call. */ - function rebaseOptIn() public nonReentrant { + function rebaseOptIn() external { _rebaseOptIn(msg.sender); } 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)); + uint256 balance = balanceOf(_account); - // Decreasing non rebasing supply - nonRebasingSupply = nonRebasingSupply.sub(balanceOf(_account)); - - _creditBalances[_account] = newCreditBalance; + // prettier-ignore + require( + alternativeCreditsPerToken[_account] > 0 || + // Accounts may explicitly `rebaseOptIn` regardless of + // accounting if they have a 0 balance. + creditBalances[_account] == 0 + , + "Account must be non-rebasing" + ); + RebaseOptions state = rebaseState[_account]; + // prettier-ignore + require( + state == RebaseOptions.StdNonRebasing || + state == RebaseOptions.NotSet, + "Only standard non-rebasing accounts can opt in" + ); - // Increase rebasing credits, totalSupply remains unchanged so no - // adjustment necessary - _rebasingCredits = _rebasingCredits.add(_creditBalances[_account]); + uint256 newCredits = _balanceToRebasingCredits(balance); - rebaseState[_account] = RebaseOptions.OptIn; + // Account + rebaseState[_account] = RebaseOptions.StdRebasing; + alternativeCreditsPerToken[_account] = 0; + creditBalances[_account] = newCredits; + // Globals + _adjustGlobals(newCredits.toInt256(), -balance.toInt256()); - // Delete any fixed credits per token - delete nonRebasingCreditsPerToken[_account]; emit AccountRebasingEnabled(_account); } /** - * @dev Explicitly mark that an address is non-rebasing. + * @notice The calling account will no longer receive yield */ - function rebaseOptOut() public nonReentrant { - require(!_isNonRebasingAccount(msg.sender), "Account has not opted in"); + function rebaseOptOut() external { + _rebaseOptOut(msg.sender); + } + + 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" + ); - // Increase non rebasing supply - nonRebasingSupply = nonRebasingSupply.add(balanceOf(msg.sender)); - // Set fixed credits per token - nonRebasingCreditsPerToken[msg.sender] = _rebasingCreditsPerToken; + uint256 oldCredits = creditBalances[_account]; + uint256 balance = balanceOf(_account); - // Decrease rebasing credits, total supply remains unchanged so no - // adjustment necessary - _rebasingCredits = _rebasingCredits.sub(_creditBalances[msg.sender]); + // Account + rebaseState[_account] = RebaseOptions.StdNonRebasing; + alternativeCreditsPerToken[_account] = 1e18; + creditBalances[_account] = balance; + // Globals + _adjustGlobals(-oldCredits.toInt256(), balance.toInt256()); - // Mark explicitly opted out of rebasing - rebaseState[msg.sender] = RebaseOptions.OptOut; - emit AccountRebasingDisabled(msg.sender); + 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 - nonReentrant - { - require(_totalSupply > 0, "Cannot increase 0 supply"); + function changeSupply(uint256 _newTotalSupply) external onlyVault { + require(totalSupply > 0, "Cannot increase 0 supply"); - if (_totalSupply == _newTotalSupply) { + if (totalSupply == _newTotalSupply) { emit TotalSupplyUpdatedHighres( - _totalSupply, - _rebasingCredits, - _rebasingCreditsPerToken + totalSupply, + rebasingCredits_, + rebasingCreditsPerToken_ ); return; } - _totalSupply = _newTotalSupply > MAX_SUPPLY + totalSupply = _newTotalSupply > MAX_SUPPLY ? MAX_SUPPLY : _newTotalSupply; - _rebasingCreditsPerToken = _rebasingCredits.divPrecisely( - _totalSupply.sub(nonRebasingSupply) + uint256 rebasingSupply = totalSupply - nonRebasingSupply; + // round up in the favour of the protocol + rebasingCreditsPerToken_ = + (rebasingCredits_ * 1e18 + rebasingSupply - 1) / + rebasingSupply; + + require(rebasingCreditsPerToken_ > 0, "Invalid change in supply"); + + emit TotalSupplyUpdatedHighres( + totalSupply, + rebasingCredits_, + rebasingCreditsPerToken_ ); + } - require(_rebasingCreditsPerToken > 0, "Invalid change in supply"); + /* + * @notice Send the yield from one account to another account. + * Each account keeps its 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"); - _totalSupply = _rebasingCredits - .divPrecisely(_rebasingCreditsPerToken) - .add(nonRebasingSupply); + 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]; - emit TotalSupplyUpdatedHighres( - _totalSupply, - _rebasingCredits, - _rebasingCreditsPerToken + 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 (alternativeCreditsPerToken[_from] == 0) { + _rebaseOptOut(_from); + } + if (alternativeCreditsPerToken[_to] > 0) { + _rebaseOptIn(_to); + } + + 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; + + // Global + int256 creditsChange = newToCredits.toInt256() - oldToCredits.toInt256(); + _adjustGlobals(creditsChange, -(fromBalance).toInt256()); + emit YieldDelegated(_from, _to); + } + + /* + * @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 + require(yieldTo[_from] != address(0), "Zero address not allowed"); + + 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); + + // 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] = newToCredits; + + // Global + int256 creditsChange = newToCredits.toInt256() - oldToCredits.toInt256(); + _adjustGlobals(creditsChange, fromBalance.toInt256()); + emit YieldUndelegated(_from, to); } } diff --git a/contracts/contracts/token/README-token-logic.md b/contracts/contracts/token/README-token-logic.md new file mode 100644 index 0000000000..dab52183b0 --- /dev/null +++ b/contracts/contracts/token/README-token-logic.md @@ -0,0 +1,213 @@ +# 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 seamlessly transfer all earned yield to another account. + +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. 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. + +**_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 + +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 receives 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 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: + +- `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 + +## 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 valued `YieldFrom` points to an account that has a `YieldTo` pointing back to the starting account. + +## 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. [^1] + + +> The balanceOf on each account equals `_creditBalances[account] * (alternativeCreditsPerToken[account] > 0 ? alternativeCreditsPerToken[account] : _rebasingCreditsPerToken) - (yieldFrom[account] == 0 ? 0 : _creditBalances[yieldFrom[account]])` + + +## Rebasing + +The token distributes yield to users by "rebasing" (changing supply). This leaves all non-rebasing users with the same account balance. + +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 + + +> After a call to changeSupply() then `nonRebasingCredits + (rebasingCredits / rebasingCreditsPer) <= totalSupply` + + +> After a non-reverting call to changeSupply(), the new totalSupply should always match what was passed into the call. + + +> Only transfers, mints, and burns change the balance of `StdNonRebasing` and `YieldDelegationSource` accounts. + + +## Other invariants + + +> 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 + + +> 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. + +[^2]: As long as the token has sufficient resolution \ No newline at end of file diff --git a/contracts/deploy/base/021_upgrade_oeth.js b/contracts/deploy/base/021_upgrade_oeth.js new file mode 100644 index 0000000000..c2fd99fae3 --- /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("OETHBase"); + + const cOETHbProxy = await ethers.getContract("OETHBaseProxy"); + + return { + actions: [ + { + // 1. Upgrade OETH + contract: cOETHbProxy, + signature: "upgradeTo(address)", + args: [dOETHb.address], + }, + ], + }; + } +); diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index 4944a896e5..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 { @@ -1172,9 +1173,7 @@ const deployOETHCore = async () => { */ const resolution = ethers.utils.parseUnits("1", 27); await withConfirmation( - cOETH - .connect(sGovernor) - .initialize("Origin Ether", "OETH", cOETHVaultProxy.address, resolution) + cOETH.connect(sGovernor).initialize(cOETHVaultProxy.address, resolution) ); log("Initialized OETH"); }; @@ -1192,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"); @@ -1260,9 +1264,7 @@ const deployOUSDCore = async () => { */ const resolution = ethers.utils.parseUnits("1", 27); await withConfirmation( - cOUSD - .connect(sGovernor) - .initialize("Origin Dollar", "OUSD", cVaultProxy.address, resolution) + cOUSD.connect(sGovernor).initialize(cVaultProxy.address, resolution) ); log("Initialized OUSD"); }; 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/115_ousd_upgrade.js b/contracts/deploy/mainnet/115_ousd_upgrade.js new file mode 100644 index 0000000000..2c4bac2d94 --- /dev/null +++ b/contracts/deploy/mainnet/115_ousd_upgrade.js @@ -0,0 +1,34 @@ +const { deploymentWithGovernanceProposal } = require("../../utils/deploy"); + +module.exports = deploymentWithGovernanceProposal( + { + deployName: "115_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], + }, + ], + }; + } +); diff --git a/contracts/deploy/mainnet/116_oeth_upgrade.js b/contracts/deploy/mainnet/116_oeth_upgrade.js new file mode 100644 index 0000000000..7bbcc7ed6f --- /dev/null +++ b/contracts/deploy/mainnet/116_oeth_upgrade.js @@ -0,0 +1,38 @@ +const { deploymentWithGovernanceProposal } = require("../../utils/deploy"); + +module.exports = deploymentWithGovernanceProposal( + { + deployName: "116_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", [], undefined, true); + + // 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], + }, + ], + }; + } +); 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..2adf9ca802 100644 --- a/contracts/docs/OUSDStorage.svg +++ b/contracts/docs/OUSDStorage.svg @@ -4,92 +4,74 @@ - - + + StorageDiagram - + 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) + +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) 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 diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index 71120df1b8..bd3645b8fe 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,294 @@ 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); } + 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,31 +493,52 @@ const defaultFixture = deployments.createFixture(async () => { const { governorAddr, strategistAddr, timelockAddr } = await getNamedAccounts(); - const ousdProxy = await ethers.getContract("OUSDProxy"); - const vaultProxy = await ethers.getContract("VaultProxy"); - - const compoundStrategyProxy = await ethers.getContract( - "CompoundStrategyProxy" - ); + const vaultAndTokenConracts = await getVaultAndTokenConracts(); - const ousd = await ethers.getContractAt("OUSD", ousdProxy.address); - const vault = await ethers.getContractAt("IVault", vaultProxy.address); + const signers = await hre.ethers.getSigners(); + let governor = signers[1]; + let strategist = signers[0]; - 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 accountTypes = await createAccountTypes({ + ousd: vaultAndTokenConracts.ousd, + ousdUnlocked: vaultAndTokenConracts.ousdUnlocked, + vault: vaultAndTokenConracts.vault, + deploy: deployments.deploy, + }); - let woeth, woethProxy; + return { + ...vaultAndTokenConracts, + ...accountTypes, + governorAddr, + strategistAddr, + timelockAddr, + governor, + strategist, + }; +}); - if (isFork) { - woethProxy = await ethers.getContract("WOETHProxy"); - woeth = await ethers.getContractAt("WOETH", woethProxy.address); +const defaultFixture = deployments.createFixture(async () => { + if (!snapshotId && !isFork) { + snapshotId = await nodeSnapshot(); } + log(`Forked from block: ${await hre.ethers.provider.getBlockNumber()}`); + + 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( "Harvester", @@ -253,6 +558,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 @@ -367,8 +677,6 @@ const defaultFixture = deployments.createFixture(async () => { morphoSteakHouseUSDCVault, morphoGauntletPrimeUSDCVault, morphoGauntletPrimeUSDTVault, - mockNonRebasing, - mockNonRebasingTwo, LUSD, fdai, fusdt, @@ -647,12 +955,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( @@ -683,10 +985,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(); @@ -712,11 +1016,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, @@ -730,13 +1039,9 @@ const defaultFixture = deployments.createFixture(async () => { timelock, oldTimelock, // Contracts - ousd, - vault, vaultValueChecker, harvester, dripper, - mockNonRebasing, - mockNonRebasingTwo, // Oracle chainlinkOracleFeedDAI, chainlinkOracleFeedUSDT, @@ -818,9 +1123,7 @@ const defaultFixture = deployments.createFixture(async () => { fusdt, // OETH - oethVault, oethVaultValueChecker, - oeth, frxETH, sfrxETH, sDAI, @@ -831,7 +1134,6 @@ const defaultFixture = deployments.createFixture(async () => { lidoWithdrawalStrategy, balancerREthStrategy, oethMorphoAaveStrategy, - woeth, convexEthMetaStrategy, oethDripper, oethHarvester, @@ -2684,6 +2986,7 @@ module.exports = { resetAllowance, defaultFixture, oethDefaultFixture, + loadTokenTransferFixture, mockVaultFixture, compoundFixture, compoundVaultFixture, diff --git a/contracts/test/flipper/flipper.js b/contracts/test/flipper/flipper.js index 8941c2d15a..239708bab8 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/token/ousd.js b/contracts/test/token/ousd.js index caea0ad059..4c48cd7245 100644 --- a/contracts/test/token/ousd.js +++ b/contracts/test/token/ousd.js @@ -1,9 +1,11 @@ const { expect } = require("chai"); -const { loadDefaultFixture } = require("../_fixture"); -const { utils } = require("ethers"); +const { loadDefaultFixture, loadTokenTransferFixture } = require("../_fixture"); +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 - 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 +107,16 @@ 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 () => { @@ -248,9 +259,8 @@ describe("Token", function () { let { ousd, vault, matt, usdc, josh, mockNonRebasing } = fixture; // Give Josh an allowance to move Matt's OUSD - await ousd - .connect(matt) - .increaseAllowance(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 .connect(josh) @@ -268,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 - 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 @@ -286,9 +300,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) - .increaseAllowance(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) @@ -333,10 +345,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 matt.getAddress(), - ousdUnits("100") - ); + await mockNonRebasing.approve(await matt.getAddress(), ousdUnits("100")); await ousd .connect(matt) @@ -380,10 +389,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 matt.getAddress(), - ousdUnits("150") - ); + await mockNonRebasing.approve(await matt.getAddress(), ousdUnits("150")); await ousd .connect(matt) @@ -408,6 +414,18 @@ 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(zeroAddress) + ).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; @@ -442,11 +460,15 @@ 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) + 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)) @@ -497,18 +519,53 @@ 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 } = fixture; + let { ousd, matt, usdc } = fixture; + await usdc.connect(matt).mint(usdcUnits("2")); + await expect(ousd.connect(matt).rebaseOptIn()).to.be.revertedWith( - "Account has not opted out" + "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(); await expect(ousd.connect(matt).rebaseOptOut()).to.be.revertedWith( - "Account has not opted in" + "Account must be rebasing" ); }); @@ -516,17 +573,26 @@ describe("Token", function () { let { mockNonRebasing } = fixture; await mockNonRebasing.rebaseOptIn(); await expect(mockNonRebasing.rebaseOptIn()).to.be.revertedWith( - "Account has not opted out" + "Only standard non-rebasing accounts can opt in" ); }); 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 has not opted in" + "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; @@ -615,40 +681,7 @@ describe("Token", function () { await anna.getAddress(), ousdUnits("100") ) - ).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")); + ).to.be.revertedWith("Allowance exceeded"); }); it("Should increase users balance on supply increase", async () => { @@ -665,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 () => { @@ -818,7 +865,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 @@ -826,7 +873,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 @@ -849,4 +896,206 @@ describe("Token", function () { await checkTransferOut(5); await checkTransferOut(9); }); + + describe("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 + .delegateYield(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); + + await ousd.connect(matt).transfer(josh.address, ousdUnits("80")); + 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 () => { + 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); + + // TODO: delete rebase opt out later + await ousd.connect(matt).rebaseOptOut(); + await ousd + .connect(governor) + // matt delegates yield to anna + .delegateYield(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 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 () { + 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.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); + }); + + 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); + 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); + 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"); + }); + + 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"); + }); + }); }); diff --git a/contracts/test/token/token-transfers.js b/contracts/test/token/token-transfers.js new file mode 100644 index 0000000000..c8d87bc0ee --- /dev/null +++ b/contracts/test/token/token-transfers.js @@ -0,0 +1,338 @@ +const { expect } = require("chai"); +const { loadTokenTransferFixture } = require("../_fixture"); + +const { isFork, ousdUnits } = require("../helpers"); + +describe("Account type variations", 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", + balancePartOfRebasingCredits: true, + isContract: false, + inYieldDelegation: false, + }, + { + name: "rebase_eoa_stdRebasing_0", + balancePartOfRebasingCredits: true, + isContract: false, + inYieldDelegation: false, + }, + { + name: "rebase_contract_0", + balancePartOfRebasingCredits: true, + isContract: true, + inYieldDelegation: false, + }, + { + name: "nonrebase_eoa_0", + balancePartOfRebasingCredits: false, + isContract: false, + inYieldDelegation: false, + }, + { + name: "nonrebase_cotract_0", + balancePartOfRebasingCredits: false, + isContract: true, + inYieldDelegation: false, + }, + { + name: "nonrebase_cotract_notSet_0", + balancePartOfRebasingCredits: false, + skipTransferTest: true, + isContract: true, + inYieldDelegation: false, + }, + { + name: "nonrebase_cotract_notSet_altcpt_gt_0", + balancePartOfRebasingCredits: false, + isContract: true, + inYieldDelegation: false, + }, + { + name: "rebase_delegate_source_0", + balancePartOfRebasingCredits: true, + isContract: false, + inYieldDelegation: true, + }, + { + name: "rebase_delegate_target_0", + balancePartOfRebasingCredits: true, + isContract: false, + inYieldDelegation: true, + }, + ]; + + const toAccounts = [ + { + 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", + balancePartOfRebasingCredits: false, + inYieldDelegation: false, + }, + { + name: "rebase_delegate_source_1", + balancePartOfRebasingCredits: true, + inYieldDelegation: true, + }, + { + name: "rebase_delegate_target_1", + balancePartOfRebasingCredits: true, + inYieldDelegation: 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, + balancePartOfRebasingCredits: fromAffectsRC, + skipTransferTest, + isContract, + } = fromAccounts[i]; + 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; + + 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 + ); + } + ); + } + } + + 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( + expectedNonRebasingSupply + ); + } + ); + } + } +}); 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 () => {