Last active
December 30, 2021 11:18
-
-
Save brockelmore/c7a497fc5bdf5a65f418fee982121f17 to your computer and use it in GitHub Desktop.
Root storage pattern
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// SPDX-License-Identifier: Unlicense | |
pragma solidity 0.8.10; | |
import "solmate/tokens/ERC20.sol"; | |
import "solmate/utils/SafeTransferLib.sol"; | |
import "ds-test/test.sol"; | |
contract MerkleTrust is DSTest { | |
using SafeTransferLib for ERC20; | |
ERC20 token; | |
mapping(address => bytes32) public userRoots; | |
constructor(address t) { | |
token = ERC20(t); | |
} | |
struct Vouch { | |
address borrower; | |
uint256 vouchAmount; | |
uint256 outstanding; | |
} | |
struct Staker { | |
address staker; | |
uint256 totalStaked; | |
uint256 totalBorrowed; | |
Vouch[] vouches; | |
} | |
function debug(string memory loc, MerkleTrust.Staker memory staker) internal { | |
emit log_named_string(string(abi.encodePacked(loc, " staker")), "----"); | |
emit log_named_address("\tstaker", staker.staker); | |
emit log_named_uint("\tborrowed", staker.totalBorrowed); | |
emit log_named_uint("\tstaked", staker.totalStaked); | |
for (uint256 i = 0; i < staker.vouches.length; i++) { | |
emit log_named_string("\tvouch", "---"); | |
emit log_named_uint("\t\tvouch index", i); | |
emit log_named_address("\t\tborrower", staker.vouches[i].borrower); | |
emit log_named_uint("\t\toutstanding", staker.vouches[i].outstanding); | |
emit log_named_uint("\t\tvouchAmount", staker.vouches[i].vouchAmount); | |
} | |
emit log_named_string("staker", "----"); | |
} | |
event LastestStakerUpdate(address indexed updatedStaker, Staker staker); | |
event VouchUpdate(address indexed borrower, address indexed staker, Vouch); | |
function withdraw(uint256 amount, Staker memory staker) external { | |
require(root(staker) == userRoots[msg.sender], "!root"); | |
require(amount < (staker.totalStaked - staker.totalBorrowed), "!liquid"); | |
staker.totalStaked -= amount; | |
staker = clampVouches(staker); | |
userRoots[msg.sender] = root(staker); | |
token.safeTransfer(msg.sender, amount); | |
emit LastestStakerUpdate(msg.sender, staker); | |
} | |
function stake(uint256 amount, Staker memory staker) public { | |
if (userRoots[msg.sender] == bytes32(0)) { | |
Staker memory staker = Staker({ | |
staker: msg.sender, | |
totalStaked: amount, | |
totalBorrowed: 0, | |
vouches: new Vouch[](0) | |
}); | |
userRoots[msg.sender] = root(staker); | |
emit LastestStakerUpdate(msg.sender, staker); | |
} else { | |
require(root(staker) == userRoots[msg.sender], "!root"); | |
staker.totalStaked += amount; | |
userRoots[msg.sender] = root(staker); | |
emit LastestStakerUpdate(msg.sender, staker); | |
} | |
token.safeTransferFrom(msg.sender, address(this), amount); | |
} | |
function borrow(uint256 amount, uint256[] memory vouchIndexes, Staker[] memory stakers) external { | |
require(vouchIndexes.length == stakers.length, "!len parity"); | |
uint256 remainingAmount = amount; | |
uint256 i = 0; | |
uint256 len = stakers.length; | |
while (remainingAmount > 0) { | |
require(i < len, "end"); | |
Staker memory staker = stakers[i]; | |
require(root(staker) == userRoots[staker.staker], "!root"); | |
uint256 index = vouchIndexes[i]; | |
require(staker.vouches[index].borrower == msg.sender, "!vouch"); | |
uint256 vouchRemaining = staker.vouches[index].vouchAmount - staker.vouches[index].outstanding; | |
uint256 borrowRemaining = staker.totalStaked - staker.totalBorrowed; | |
if (vouchRemaining >= remainingAmount) { | |
if (borrowRemaining >= remainingAmount) { | |
staker.vouches[index].outstanding += remainingAmount; | |
staker.totalBorrowed += remainingAmount; | |
remainingAmount = 0; | |
} else { | |
staker.vouches[index].outstanding += borrowRemaining; | |
staker.totalBorrowed += borrowRemaining; | |
remainingAmount -= borrowRemaining; | |
} | |
} else { | |
if (borrowRemaining >= vouchRemaining) { | |
staker.vouches[index].outstanding = staker.vouches[index].vouchAmount; | |
staker.totalBorrowed += vouchRemaining; | |
remainingAmount -= vouchRemaining; | |
} else { | |
staker.vouches[index].outstanding += borrowRemaining; | |
staker.totalBorrowed += borrowRemaining; | |
remainingAmount -= borrowRemaining; | |
} | |
} | |
userRoots[staker.staker] = root(staker); | |
emit LastestStakerUpdate(staker.staker, staker); | |
++i; | |
} | |
token.safeTransfer(msg.sender, amount); | |
} | |
function borrowSingle(uint256 amount, uint256 vouchIndex, Staker memory staker) external { | |
require(root(staker) == userRoots[staker.staker], "!root"); | |
require(msg.sender == staker.vouches[vouchIndex].borrower, "!vouch"); | |
require(amount <= (staker.vouches[vouchIndex].vouchAmount - staker.vouches[vouchIndex].outstanding), "!remaining"); | |
require(amount <= (staker.totalStaked - staker.totalBorrowed), "!liquid"); | |
// all can be borrowed from a single person | |
staker.vouches[vouchIndex].outstanding += amount; | |
staker.totalBorrowed += amount; | |
// update root | |
userRoots[staker.staker] = root(staker); | |
token.safeTransfer(msg.sender, amount); | |
emit LastestStakerUpdate(staker.staker, staker); | |
} | |
function repayBorrows(uint256 amount, uint256[] memory vouchIndexes, Staker[] memory stakers) external { | |
require(vouchIndexes.length == stakers.length, "!len parity"); | |
uint256 remainingAmount = amount; | |
uint256 i = 0; | |
uint256 len = stakers.length; | |
while (remainingAmount > 0) { | |
require(i < len, "end"); | |
Staker memory staker = stakers[i]; | |
require(root(staker) == userRoots[staker.staker], "!root"); | |
uint256 index = vouchIndexes[i]; | |
require(staker.vouches[index].borrower == msg.sender, "!vouch"); | |
uint256 outstanding = staker.vouches[index].outstanding; | |
if (remainingAmount > outstanding) { | |
remainingAmount -= staker.vouches[index].outstanding; | |
staker.vouches[index].outstanding = 0; | |
} else { | |
staker.vouches[index].outstanding -= remainingAmount; | |
remainingAmount = 0; | |
} | |
userRoots[staker.staker] = root(staker); | |
emit LastestStakerUpdate(staker.staker, staker); | |
++i; | |
} | |
token.safeTransferFrom(msg.sender, address(this), amount); | |
} | |
function updateVouches(Staker memory staker, Vouch[] memory newVouches) external { | |
if (userRoots[msg.sender] == bytes32(0)) { | |
Staker memory newStaker = initStaker(msg.sender, newVouches); | |
logVouchUpdates(newVouches); | |
userRoots[msg.sender] = root(newStaker); | |
emit LastestStakerUpdate(msg.sender, newStaker); | |
} else { | |
require(root(staker) == userRoots[msg.sender], "!root"); | |
Vouch[] memory vouches = new Vouch[](staker.vouches.length + newVouches.length); | |
bool[] memory foundIndexes = new bool[](staker.vouches.length); | |
uint256 totalVouchedAmount = 0; | |
uint256 nextInsert = staker.vouches.length; | |
for (uint256 i = 0; i < newVouches.length; i++) { | |
require(newVouches[i].vouchAmount <= staker.totalStaked, "overvouch"); | |
uint256 index = indexOfVouch(staker, newVouches[i]); | |
uint256 finalIndex = index; | |
if (index != type(uint256).max) { | |
foundIndexes[index] = true; | |
// only able to update vouch amount | |
vouches[index] = staker.vouches[index]; | |
vouches[index].vouchAmount = newVouches[i].vouchAmount; | |
vouches[index].borrower = newVouches[i].borrower; | |
} else { | |
finalIndex = nextInsert; | |
vouches[nextInsert].vouchAmount = newVouches[i].vouchAmount; | |
vouches[nextInsert].borrower = newVouches[i].borrower; | |
nextInsert += 1; | |
} | |
emit VouchUpdate(newVouches[i].borrower, msg.sender, vouches[finalIndex]); | |
} | |
for (uint256 i = 0; i < staker.vouches.length; i++) { | |
if (!foundIndexes[i]) { | |
vouches[i] = staker.vouches[i]; | |
} | |
} | |
staker.vouches = vouches; | |
userRoots[staker.staker] = root(staker); | |
emit LastestStakerUpdate(msg.sender, staker); | |
} | |
} | |
function updatedVouches(Staker memory staker, Vouch[] memory newVouches) external view returns (Staker memory) { | |
Vouch[] memory vouches = new Vouch[](staker.vouches.length + newVouches.length); | |
bool[] memory foundIndexes = new bool[](staker.vouches.length); | |
uint256 totalVouchedAmount = 0; | |
uint256 nextInsert = staker.vouches.length; | |
for (uint256 i = 0; i < newVouches.length; i++) { | |
require(newVouches[i].vouchAmount <= staker.totalStaked, "overvouch"); | |
uint256 index = indexOfVouch(staker, newVouches[i]); | |
uint256 finalIndex = index; | |
if (index != type(uint256).max) { | |
foundIndexes[index] = true; | |
// only able to update vouch amount | |
vouches[index] = staker.vouches[index]; | |
vouches[index].vouchAmount = newVouches[i].vouchAmount; | |
vouches[index].borrower = newVouches[i].borrower; | |
} else { | |
finalIndex = nextInsert; | |
vouches[nextInsert].vouchAmount = newVouches[i].vouchAmount; | |
vouches[nextInsert].borrower = newVouches[i].borrower; | |
nextInsert += 1; | |
} | |
} | |
for (uint256 i = 0; i < staker.vouches.length; i++) { | |
if (!foundIndexes[i]) { | |
vouches[i] = staker.vouches[i]; | |
} | |
} | |
staker.vouches = vouches; | |
return staker; | |
} | |
function root(Staker memory staker) public returns (bytes32) { | |
return keccak256(abi.encode(staker)); | |
} | |
// internal functions | |
function clampVouches(Staker memory staker) internal returns (Staker memory) { | |
uint256 maxVouch = staker.totalStaked; | |
for (uint256 i = 0; i < staker.vouches.length; i++) { | |
require(staker.vouches[i].outstanding <= maxVouch, "outstanding"); | |
if (staker.vouches[i].vouchAmount > maxVouch) { | |
staker.vouches[i].vouchAmount = maxVouch; | |
emit VouchUpdate(staker.vouches[i].borrower, staker.staker, staker.vouches[i]); | |
} | |
} | |
return staker; | |
} | |
function initStaker(address who, Vouch[] memory vouches) internal returns (Staker memory) { | |
Staker memory staker = Staker({ | |
staker: who, | |
totalStaked: 0, | |
totalBorrowed: 0, | |
vouches: vouches | |
}); | |
return staker; | |
} | |
function logVouchUpdates(Vouch[] memory newVouches) internal { | |
for (uint256 i = 0; i < newVouches.length; i++) { | |
emit VouchUpdate(newVouches[i].borrower, msg.sender, newVouches[i]); | |
} | |
} | |
function indexOfVouch(Staker memory staker, Vouch memory vouch) internal pure returns (uint256) { | |
uint256 len = staker.vouches.length; | |
for (uint256 i = 0; i < len; i++) { | |
if (staker.vouches[i].borrower == vouch.borrower) { | |
return i; | |
} | |
} | |
return type(uint256).max; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This is some very pretty code!! Really like this implementation as this is a more efficient way of updating the credit network for each staker. One question should
staker.totalBorrowed
be updated in repay? It's incremented when you borrow and seems to be the sum of all vouchersoutstanding
. But when outstanding gets decremented in repaytotalBorrowed
does not. (not discounting the possibility that I've goofed and how you have it is in fact how it should be 🤔) .... PS what you doing for the next 6 months 😂