Skip to content

Instantly share code, notes, and snippets.

@brockelmore
Last active December 30, 2021 11:18
Show Gist options
  • Save brockelmore/c7a497fc5bdf5a65f418fee982121f17 to your computer and use it in GitHub Desktop.
Save brockelmore/c7a497fc5bdf5a65f418fee982121f17 to your computer and use it in GitHub Desktop.
Root storage pattern
// 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;
}
}
@GeraldHost
Copy link

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 vouchers outstanding. But when outstanding gets decremented in repay totalBorrowed 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 😂

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment