Skip to content

Create idea.md #742

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

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Changes from all 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
164 changes: 164 additions & 0 deletions idea.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# Payment Splitting

## How to calculate the withdrawable amount.

The following is explanation the concept of calculating the amount of tokens a user can withdraw (=`payment`) using pseudocode in JS syntax.

ref: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.9.6/contracts/finance/PaymentSplitter.sol

```javascript!
const total_released_tokens = new Map([['TOKEN', 0]]);
const released_tokens_of_user = new Map([['TOKEN', new Map([['USER', 0]])]]);

function withdraw(token, user) {
const Property = ERC20(property);
const Token = ERC20(token);
const total_received =
Token.balanceOf(address(this)) +
total_released_tokens.get(token);

/*
*/
const released = released_tokens_of_user.get(token).get(user);
Copy link
Member Author

Choose a reason for hiding this comment

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

It is this variable released that is important to prevent double payments. The contract needs to copy/increase released from sender to recipient.


const user_balance = Property.balanceOf(user);
const total_supply = Property.totalSupply();
const payment =
(total_received * user_balance) / total_supply - released;

/*
* (TRANSFER TOKENS TO THE USER HERE...)
*/
ERC20.safeTransfer(token, user, payment);

/*
* Update the global states.
*/
released_tokens_of_user.get(token).set(user, released + payment)
total_released_tokens.set(
token,
total_released_tokens.get(token) + payment
);

return payment;
}
```

## Cases

The withdrawable amount is calculated from the latest share of Property.

### Case-1
|_timeline_|_event_|Alice|Bob|Locked Value|
|---|---|---|---|---|
|1||50%|50%|0|
|2|+©100|50%|50%|100|
|3|Alice 20% → Bob|30%|70%|100|
|4|Alice withdraws<br/>Alice earns ©30|30%|70%|70|
|5|Bob withdraws<br/>Bob earns ©70|30%|70%|0|

### Case-2
|_timeline_|_event_|Alice|Bob|Locked Value|
|---|---|---|---|---|
|1||50%|50%|0|
|2|+©100|50%|50%|100|
|3|Alice withdraws<br/>Alice earns ©50|50%|50%|50|
|4|Alice 20% → Bob|30%|70%|50|
|5|+©200|30%|70%|250|
|6|Bob withdraws<br/>Bob earns ©210|30%|70%|40|
|7|Alice withdraws<br/>Alice earns ©40|30%|70%|0|

---

# Idea

ref: https://github.com/dev-protocol/protocol-v2/pull/714

### Vault side

Add a call for updating `released` and a local state that records the user balance at the time of the last withdraw.

```diff
const total_released_tokens = new Map([['TOKEN', 0]]);
const released_tokens_of_user = new Map([['TOKEN', new Map([['USER', 0]])]]);
+ const last_balance_of_user_for_tokens = new Map([['TOKEN', new Map([['USER', 0]])]]);

function withdraw(token, user) {
const Property = ERC20(property);
const Token = ERC20(token);
const total_received =
Token.balanceOf(address(this)) +
total_released_tokens.get(token);

+ updateReleasedTokens(token, user, Property.balanceOf(user));

/*
*/
const released = released_tokens_of_user.get(token).get(user);

const user_balance = Property.balanceOf(user);
const total_supply = Property.totalSupply();
const payment =
(total_received * user_balance) / total_supply - released;

/*
* (TRANSFER TOKENS TO THE USER HERE...)
*/
ERC20.safeTransfer(token, user, payment);

/*
* Update the global states.
*/
released_tokens_of_user.get(token).set(user, released + payment)
total_released_tokens.set(
token,
total_released_tokens.get(token) + payment
);

+ last_balance_of_user_for_tokens[token][user] = user_balance;
Copy link
Member Author

Choose a reason for hiding this comment

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

At the end of withdraw transaction, the user's token balance is recorded here. This value will always be referenced inwithdraw transaction to detect any change in the balance.


return payment;
}
```

`updateReleasedTokens` updates the value of `released` by referencing `Withdraw.transferHistory` recursively until `last_balance_of_user_for_tokens` matches the latest balance.

```javascript!
const released_tokens_addresses = new Map([['USER', new Set()]]); // mapping(address => EnumerableSet.AddressSet)
const Withdraw = Withdraw(Registry(registry).registries('Withdraw'));

function updateReleasedTokens(token, user, current_balance) {
if (last_balance_of_user_for_tokens[token][user] === current_balance) {
Copy link
Member Author

Choose a reason for hiding this comment

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

If the recorded balance and the latest balance match, nothing is done.

// No need to update it.
return
}

const historyIndex = Withdraw.transferHistoryLengthOfRecipient(address(Property), user);
let done = false;
let i = historyIndex;
let calculated = 0;
while (!done || i >= 0) {
const history = Withdraw.transferHistory(
Withdraw.transferHistoryOfRecipientByIndex(i)
);
if (!history.fill) {
// This is always done on the history of the last 1 transfer, and other case, history.fill is true.
history.amount = current_balance - history.preBalanceOfRecipient;
}

updateReleasedTokens(token, history.from, Property.balanceOf(history.from));
Copy link
Member

Choose a reason for hiding this comment

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

We are making recursive call here which will re-trigger the loop from the beginning without updating the released_tokens_of_user state. So, base case might get skipped and it can get stuck in infinite loop?


const released_tokens = released_tokens_of_user.get(token).get(history.from);
const part_of_released = released_tokens * history.preBalanceOfSender / history.amount;
Copy link
Member Author

Choose a reason for hiding this comment

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

To copy the sender's released to the recipient, calculate part_of_released .

released_tokens_of_user.get(token).set(
user,
part_of_released + released_tokens_of_user.get(token).get(user)
Copy link
Member Author

Choose a reason for hiding this comment

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

Add part_of_released to the recipient's released.

);

i = i - 1;
calculated = calculated + history.amount;
done = last_balance_of_user_for_tokens[token][user] + calculated === current_balance;
Copy link
Member Author

Choose a reason for hiding this comment

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

If the calculated transfers + recorded balance matches the user's latest balance, the loop ends.

}
}
```

Loading