Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rebase to another account #2245

Closed
wants to merge 21 commits into from
Closed
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
187 changes: 184 additions & 3 deletions contracts/contracts/token/OUSD.sol
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@
enum RebaseOptions {
NotSet,
OptOut,
OptIn
OptIn,
// for delegated yield accounts
Delegate
}

uint256 private constant MAX_SUPPLY = ~uint128(0); // (2^128) - 1
Expand All @@ -52,9 +54,72 @@
mapping(address => uint256) public nonRebasingCreditsPerToken;
mapping(address => RebaseOptions) public rebaseState;
mapping(address => uint256) public isUpgraded;
/**
* The delegatedRebases contains a mapping of:
* rebaseSource => [rebaseReceiver, creditsPerToken]
*
* This is all the additional storage logic required to track the balances when
* the rebaseSource wants to delegate its yield to a rebaseReceiver. We do this
* using the following principle:
* - a yield delegation account freezes its own rebasing by setting RebaseOptions to
* `Delegate`. It copies the global creditsPerToken to a nonRebasingCreditsPerToken
* mapping indicating its own balance doesn't rebase any longer.
* - an entry is added to delegatedRebases:
* `delegatedRebases[rebaseSource] = [rebaseReceiver, _rebasingCreditsPerToken]
* indicating the beginning of yield collection to a delegated account. The difference
* in current global contract `_rebasingCreditsPerToken` and the credits per token
* stored in the delegatedRebases marks all the yield accrued that has been delegating.
* This way a global rebase an O(1) action manages to update the rebasing tokens and
* the delegated rebase tokens. Without any other storage slot changes while rebasing.
* - IMPORTANT (!) this mapping is valid only as long as the `_creditBalances[rebaseSource]`
* doesn't change. If transfer in/out happens the `Yield accounting Action` is triggered.
* - There are 4 types of Transfer functions to consider:
* -> Transfer TO rebaseSource
* Transfer changes the amount of source credits that are rebasing to a receiverAccount.
* Yield accounting action triggered
* -> Transfer FROM rebaseSource
* Transfer changes the amount of source credits that are rebasing to a receiverAccount.
* Yield accounting action triggered
* -> Transfer TO rebaseReceiver
* The delegated credits need not be touched here. We just update the internal credits
* of the rebaseReceiver
* -> Transfer FROM rebaseReceiver
* Receiver has credits from 2 sources in the contract:
* 1. delegated yield
* 2. its own internal credits
* Its own internal credits might not be enough to facilitate the transfer.
* Yield accounting action triggered
*
* Yield accounting Action
* When a transfer from/to rebaseSrouce or transfer from rebaseReceiver happens all the
* delegated yield accruing in the `delegatedRebases` is materialized to
* _creditBalances[rebaseReceiver]. The delegatedRebases[rebaseSource]'s
* creditsPerToken are updated to the latest global contract value.
* In other words yield of an account represented by the delegation mapping is moved
* to the receiver's creditBalances.
*
* LIMITATIONS:
* - rebaseSource can delegate yield to only one rebaseReceiver
* - rebaseReceiver can only have yield delegated from one rebaseSource
*
*/
mapping(address => RebaseDelegationData) private delegatedRebases;
/**
* The delegatedRebasesReversed is just the mapping in the other direction for purposes
* of data access. Any update to delegatedRebases needs to also reflect a change in
* delegatedRebasesReversed.
*
* rebaseReceiver => [rebaseSource, creditsPerToken]
*/
mapping(address => RebaseDelegationData) private delegatedRebasesReversed;

uint256 private constant RESOLUTION_INCREASE = 1e9;

struct RebaseDelegationData {
address account; // can be either rebaseSource or rebaseReceiver
uint256 delegationStartCreditsPerToken;
}

function initialize(
string calldata _nameArg,
string calldata _symbolArg,
Expand Down Expand Up @@ -121,9 +186,14 @@
override
returns (uint256)
{
if (_creditBalances[_account] == 0) return 0;
uint256 rebaseDelegatedValue = 0;
if (_hasRebaseDelegatedTo(_account)) {
rebaseDelegatedValue = _balanceOfRebaseDelegated(_account);
}

if (_creditBalances[_account] == 0) return rebaseDelegatedValue;
return
_creditBalances[_account].divPrecisely(_creditsPerToken(_account));
_creditBalances[_account].divPrecisely(_creditsPerToken(_account)) + rebaseDelegatedValue;
}

/**
Expand All @@ -138,6 +208,7 @@
view
returns (uint256, uint256)
{
// TODO make it work with rebase delegated
uint256 cpt = _creditsPerToken(_account);
if (cpt == 1e27) {
// For a period before the resolution upgrade, we created all new
Expand Down Expand Up @@ -236,6 +307,15 @@
) internal {
bool isNonRebasingTo = _isNonRebasingAccount(_to);
bool isNonRebasingFrom = _isNonRebasingAccount(_from);
if (_delegatesRebase(_to)) {
_delegatedRebaseAccountingBySource(_to);

Check warning on line 311 in contracts/contracts/token/OUSD.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/token/OUSD.sol#L311

Added line #L311 was not covered by tests
}
if (_delegatesRebase(_from)) {
_delegatedRebaseAccountingBySource(_from);

Check warning on line 314 in contracts/contracts/token/OUSD.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/token/OUSD.sol#L314

Added line #L314 was not covered by tests
}
if (_hasRebaseDelegatedTo(_from)) {
_delegatedRebaseAccountingByReceiver(_from);
}

// Credits deducted and credited might be different due to the
// differing creditsPerToken used by each account
Expand Down Expand Up @@ -559,6 +639,107 @@
emit AccountRebasingDisabled(msg.sender);
}

function governanceDelegateYield(address _accountSource, address _accountReceiver)
public
onlyGovernor
{
_delegateYield(_accountSource, _accountReceiver);
}

function governanceStopYieldDelegation(address _accountSource)

Check warning on line 649 in contracts/contracts/token/OUSD.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/token/OUSD.sol#L649

Added line #L649 was not covered by tests
public
onlyGovernor
{
_stopDelegateYield(_accountSource);

Check warning on line 653 in contracts/contracts/token/OUSD.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/token/OUSD.sol#L653

Added line #L653 was not covered by tests
}

function _stopDelegateYield(address _accountSource) internal {
RebaseDelegationData memory delegationData = delegatedRebases[_accountSource];

Check warning on line 657 in contracts/contracts/token/OUSD.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/token/OUSD.sol#L656-L657

Added lines #L656 - L657 were not covered by tests
require(delegationData.account != address(0), "No entry found");

delete delegatedRebases[_accountSource];
delete delegatedRebasesReversed[delegationData.account];
nonRebasingCreditsPerToken[_accountSource] = 0;

Check warning on line 662 in contracts/contracts/token/OUSD.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/token/OUSD.sol#L660-L662

Added lines #L660 - L662 were not covered by tests
}

function _delegateYield(address _accountSource, address _accountReceiver) internal {
require(rebaseState[_accountSource] == RebaseOptions.OptIn ||
rebaseState[_accountSource] == RebaseOptions.NotSet, "Account not rebasing");

_resetYieldDelegation(_accountSource, _accountReceiver);
nonRebasingCreditsPerToken[_accountSource] = _rebasingCreditsPerToken;
rebaseState[_accountSource] = RebaseOptions.Delegate;
}

function _resetYieldDelegation(address _accountSource, address _accountReceiver) internal {
delegatedRebases[_accountSource] = RebaseDelegationData({
account: _accountReceiver,
delegationStartCreditsPerToken: _rebasingCreditsPerToken
});

delegatedRebasesReversed[_accountReceiver] = RebaseDelegationData({
account: _accountSource,
delegationStartCreditsPerToken: _rebasingCreditsPerToken
});
}

// moves funds from delegatedRebases to creditBalances
function _delegatedRebaseAccountingBySource(address _accountSource) internal {
RebaseDelegationData memory delegationData = delegatedRebases[_accountSource];

Check warning on line 688 in contracts/contracts/token/OUSD.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/token/OUSD.sol#L687-L688

Added lines #L687 - L688 were not covered by tests

// receiver has no pending rebases to account for
if (delegationData.delegationStartCreditsPerToken == _rebasingCreditsPerToken) {
return;

Check warning on line 692 in contracts/contracts/token/OUSD.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/token/OUSD.sol#L692

Added line #L692 was not covered by tests
}
_delegatedRebaseAccounting(_accountSource, delegationData.account, delegationData.delegationStartCreditsPerToken);

Check warning on line 694 in contracts/contracts/token/OUSD.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/token/OUSD.sol#L694

Added line #L694 was not covered by tests
}

// moves funds from delegatedRebases to creditBalances
function _delegatedRebaseAccountingByReceiver(address _accountReceiver) internal {
RebaseDelegationData memory delegationData = delegatedRebasesReversed[_accountReceiver];
// receiver has no pending rebases to account for
if (delegationData.delegationStartCreditsPerToken == _rebasingCreditsPerToken) {
return;

Check warning on line 702 in contracts/contracts/token/OUSD.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/token/OUSD.sol#L702

Added line #L702 was not covered by tests
}
_delegatedRebaseAccounting(delegationData.account, _accountReceiver, delegationData.delegationStartCreditsPerToken);
}

function _delegatedRebaseAccounting(
address _accountSource,
address _accountReceiver,
uint256 _delegationStartCreditsPerToken
) internal {
// TODO: possible to support non rebasing as well
require(rebaseState[_accountReceiver] == RebaseOptions.OptIn ||
rebaseState[_accountReceiver] == RebaseOptions.NotSet, "Account Receiver needs to support rebasing");


_creditBalances[_accountReceiver] += _balanceOfRebaseDelegated(_accountSource, _delegationStartCreditsPerToken)
.mulTruncate(_rebasingCreditsPerToken);
_resetYieldDelegation(_accountSource, _accountReceiver);
}


function _balanceOfRebaseDelegated(address _accountReceiver) internal view returns (uint256){
RebaseDelegationData memory delegationData = delegatedRebasesReversed[_accountReceiver];
return _balanceOfRebaseDelegated(delegationData.account, delegationData.delegationStartCreditsPerToken);
}

function _balanceOfRebaseDelegated(address _accountSource, uint256 _delegationStartCreditsPerToken) internal view returns (uint256){
return _creditBalances[_accountSource].divPrecisely(_rebasingCreditsPerToken) -
_creditBalances[_accountSource].divPrecisely(_delegationStartCreditsPerToken);
}

// does account have a rebase delegated to itself?
function _hasRebaseDelegatedTo(address account) internal view returns (bool){
return delegatedRebasesReversed[account].delegationStartCreditsPerToken > 0;
}

// does account have delegate rebase?
function _delegatesRebase(address account) internal view returns (bool){
return delegatedRebases[account].delegationStartCreditsPerToken > 0;
}

/**
* @dev Modify the supply without minting new tokens. This uses a change in
* the exchange rate between "credits" and OUSD tokens to change balances.
Expand Down
2 changes: 1 addition & 1 deletion contracts/contracts/vault/VaultCore.sol
Original file line number Diff line number Diff line change
Expand Up @@ -833,4 +833,4 @@ contract VaultCore is VaultInitializer {
require(x < int256(MAX_INT), "Amount too high");
return x >= 0 ? uint256(x) : uint256(-x);
}
}
}
2 changes: 1 addition & 1 deletion contracts/contracts/vault/VaultStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -254,4 +254,4 @@ contract VaultStorage is Initializable, Governable {
sstore(position, newImpl)
}
}
}
}
58 changes: 58 additions & 0 deletions contracts/test/token/ousd.js
Original file line number Diff line number Diff line change
Expand Up @@ -849,4 +849,62 @@
await checkTransferOut(5);
await checkTransferOut(9);
});

it.only("Should delegate rebase to another account", async () => {

Check failure on line 853 in contracts/test/token/ousd.js

View workflow job for this annotation

GitHub Actions / Contracts Linter

it.only not permitted
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the .only needs to be removed

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);

ousd
.connect(governor)
// matt delegates yield to anna
.governanceDelegateYield(matt.address, anna.address);

// Transfer USDC into the Vault to simulate yield
await usdc.connect(matt).transfer(vault.address, usdcUnits("200"));
await vault.rebase();

await expect(josh).has.an.approxBalanceOf("220.00", ousd);
await expect(matt).has.an.approxBalanceOf("80.00", ousd);
// 10 of own rebase + 80 from matt + 10 existing balance
await expect(anna).has.an.balanceOf("100", ousd);

await ousd.connect(anna).transfer(josh.address, ousdUnits("10"));

await expect(josh).has.an.approxBalanceOf("230.00", ousd);
await expect(matt).has.an.approxBalanceOf("80.00", ousd);
await expect(anna).has.an.balanceOf("90", ousd);
});

it.only("Should delegate rebase to another account initially having 0 balance", async () => {

Check failure on line 884 in contracts/test/token/ousd.js

View workflow job for this annotation

GitHub Actions / Contracts Linter

it.only not permitted
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);

ousd
.connect(governor)
// matt delegates yield to anna
.governanceDelegateYield(matt.address, anna.address);

// Transfer USDC into the Vault to simulate yield
await usdc.connect(matt).transfer(vault.address, usdcUnits("200"));
await vault.rebase();

await expect(josh).has.an.approxBalanceOf("200.00", ousd);
await expect(matt).has.an.approxBalanceOf("100.00", ousd);
await expect(anna).has.an.balanceOf("100", ousd);

await ousd.connect(anna).transfer(josh.address, ousdUnits("10"));

await expect(josh).has.an.approxBalanceOf("210.00", ousd);
await expect(matt).has.an.approxBalanceOf("100.00", ousd);
await expect(anna).has.an.balanceOf("90", ousd);
});
});
Loading