-
Notifications
You must be signed in to change notification settings - Fork 3
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
base: main
Are you sure you want to change the base?
Create idea.md #742
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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); | ||
|
||
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At the end of |
||
|
||
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
||
const released_tokens = released_tokens_of_user.get(token).get(history.from); | ||
const part_of_released = released_tokens * history.preBalanceOfSender / history.amount; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To copy the sender's |
||
released_tokens_of_user.get(token).set( | ||
user, | ||
part_of_released + released_tokens_of_user.get(token).get(user) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add |
||
); | ||
|
||
i = i - 1; | ||
calculated = calculated + history.amount; | ||
done = last_balance_of_user_for_tokens[token][user] + calculated === current_balance; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
} | ||
} | ||
``` | ||
|
There was a problem hiding this comment.
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/increasereleased
from sender to recipient.