Skip to content

Instantly share code, notes, and snippets.

@z0r0z
Created November 1, 2021 04:26
Show Gist options
  • Save z0r0z/0b044a6fae68d3224d17572fb6eefca4 to your computer and use it in GitHub Desktop.
Save z0r0z/0b044a6fae68d3224d17572fb6eefca4 to your computer and use it in GitHub Desktop.
minor typographic changes
// SPDX-License-Identifier: UNLICENSED
/*
███ ██ ██ █
█ █ █ █ █ █ █
█ ▀ ▄ █▄▄█ █▄▄█ █
█ ▄▀ █ █ █ █ ███▄
███ █ █ ▀
█ █
▀ ▀*/
pragma solidity >=0.8.0;
import "@gnosis.pm/safe-contracts/contracts/base/Executor.sol";
import "@gnosis.pm/safe-contracts/contracts/common/Enum.sol";
import "@openzeppelin/contracts/proxy/utils/Initializable.sol";
/// @notice Interface for Baal {memberAction} that adjusts member `shares` & `loot`.
interface IShaman {
function memberAction(
address member,
uint96 loot,
uint96 shares
) external payable returns (uint96 lootOut, uint96 sharesOut);
}
/// @title Baal ';_;'.
/// @notice Flexible guild contract inspired by Moloch DAO framework.
contract Baal is Executor, Initializable {
bool public lootPaused; /*tracks transferability of `loot` economic weight - amendable through 'period'[2] proposal*/
bool public sharesPaused; /*tracks transferability of erc20 `shares` - amendable through 'period'[2] proposal*/
bool singleSummoner; /*internal flag to gauge speedy proposal processing*/
uint8 public constant decimals = 18; /*unit scaling factor in erc20 `shares` accounting - '18' is default to match ETH & common erc20s*/
uint16 constant MAX_GUILD_TOKEN_COUNT = 400; /*maximum number of whitelistable tokens subject to {ragequit}*/
uint96 public totalLoot; /*counter for total `loot` economic weight held by `members`*/
uint96 public totalSupply; /*counter for total `members` voting `shares` with erc20 accounting*/
uint32 public flashFeeNumerator; /*tracks 'fee' numerator for {flashLoan} in {flashFee} - e.g., '1 = 0.0001%'*/
uint32 public gracePeriod; /*time delay after proposal voting period for processing*/
uint32 public minVotingPeriod; /*minimum period for voting in seconds - amendable through 'period'[2] proposal*/
uint32 public maxVotingPeriod; /*maximum period for voting in seconds - amendable through 'period'[2] proposal*/
uint256 public proposalCount; /*counter for total `proposals` submitted*/
uint256 status; /*internal reentrancy check tracking value*/
string public name; /*'name' for erc20 `shares` accounting*/
string public symbol; /*'symbol' for erc20 `shares` accounting*/
bytes32 constant DOMAIN_TYPEHASH =
keccak256(
"EIP712Domain(string name,uint chainId,address verifyingContract)"
); /*EIP-712 typehash for Baal domain*/
bytes32 constant DELEGATION_TYPEHASH =
keccak256("Delegation(address delegatee,uint nonce,uint expiry)"); /*EIP-712 typehash for Baal delegation*/
bytes32 constant PERMIT_TYPEHASH =
keccak256(
"Permit(address owner,address spender,uint value,uint nonce,uint deadline)"
); /*EIP-712 typehash for EIP-2612 {permit}*/
bytes32 constant VOTE_TYPEHASH =
keccak256("Vote(uint proposalId,bool support)"); /*EIP-712 typehash for Baal proposal vote*/
address[] guildTokens; /*array list of erc20 tokens approved on summoning or by 'whitelist'[3] `proposals` for {ragequit} claims*/
address multisendLibrary; /*address of multisend library*/
mapping(address => mapping(address => uint256)) public allowance; /*maps approved pulls of `shares` with erc20 accounting*/
mapping(address => uint256) public balanceOf; /*maps `members` accounts to `shares` with erc20 accounting*/
mapping(address => mapping(uint256 => Checkpoint)) public checkpoints; /*maps record of vote `checkpoints` for each account by index*/
mapping(address => uint256) public numCheckpoints; /*maps number of `checkpoints` for each account*/
mapping(address => address) public delegates; /*maps record of each account's `shares` delegate*/
mapping(address => uint256) public nonces; /*maps record of states for signing & validating signatures*/
mapping(address => Member) public members; /*maps `members` accounts to struct details*/
mapping(uint256 => Proposal) public proposals; /*maps `proposalCount` to struct details*/
mapping(uint256 => bool) public proposalsPassed; /*maps `proposalCount` to approval status - separated out as struct is deleted, and this value can be used by minion-like contracts*/
mapping(address => bool) public shamans; /*maps contracts approved in 'whitelist'[3] proposals for {memberAction} that mint or burn `shares`*/
event SummonComplete(
bool lootPaused,
bool sharesPaused,
uint256 gracePeriod,
uint256 minVotingPeriod,
uint256 maxVotingPeriod,
string name,
string symbol,
address[] guildTokens,
address[] shamans,
address[] summoners,
uint96[] loot,
uint96[] shares
); /*emits after Baal summoning*/
event SubmitProposal(
uint256 indexed proposal,
uint256 votingPeriod,
bytes proposalData,
string details
); /*emits after proposal is submitted*/
event SponsorProposal(
address indexed member,
uint256 indexed proposal,
uint256 indexed votingStarts
); /*emits after member has sponsored proposal*/
event SubmitVote(
address indexed member,
uint256 balance,
uint256 indexed proposal,
bool indexed approved
); /*emits after vote is submitted on proposal*/
event ProcessProposal(uint256 indexed proposal); /*emits when proposal is processed & executed*/
event Ragequit(
address indexed member,
address to,
uint96 indexed lootToBurn,
uint96 indexed sharesToBurn
); /*emits when users burn Baal `shares` and/or `loot` for given `to` account*/
event Approval(
address indexed owner,
address indexed spender,
uint256 amount
); /*emits when Baal `shares` are approved for pulls with erc20 accounting*/
event Transfer(address indexed from, address indexed to, uint256 amount); /*emits when Baal `shares` are minted, burned or transferred with erc20 accounting*/
event TransferLoot(address indexed from, address indexed to, uint96 amount); /*emits when Baal `loot` is minted, burned or transferred*/
event DelegateChanged(
address indexed delegator,
address indexed fromDelegate,
address indexed toDelegate
); /*emits when an account changes its voting delegate*/
event DelegateVotesChanged(
address indexed delegate,
uint256 previousBalance,
uint256 newBalance
); /*emits when a delegate account's voting balance changes*/
modifier nonReentrant() {
/*reentrancy guard*/
require(status == 1, "reentrant");
status = 2;
_;
status = 1;
}
modifier baalOnly() {
require(msg.sender == address(this), "!baal");
_;
}
struct Checkpoint {
/*Baal checkpoint for marking number of delegated votes*/
uint32 fromTimeStamp; /*unix time for referencing voting balance*/
uint96 votes; /*votes at given unix time*/
}
struct Member {
/*Baal membership details*/
uint96 loot; /*economic weight held by `members` - can be set on summoning & adjusted via {memberAction}*/
uint256 highestIndexYesVote; /*highest proposal index on which a member `approved`*/
mapping(uint256 => bool) voted; /*maps voting decisions on proposals by `members` account*/
}
struct Proposal {
/*Baal proposal details*/
uint32 votingPeriod; /*time for voting in seconds*/
uint32 votingStarts; /*starting time for proposal in seconds since unix epoch*/
uint32 votingEnds; /*termination date for proposal in seconds since unix epoch - derived from `votingPeriod` set on proposal*/
uint96 yesVotes; /*counter for `members` `approved` 'votes' to calculate approval on processing*/
uint96 noVotes; /*counter for `members` 'dis-approved' 'votes' to calculate approval on processing*/
bytes proposalData; /*raw data associated with state updates*/
string details; /*human-readable context for proposal*/
}
/// @notice Summon Baal with voting configuration & initial array of `members` accounts with `shares` & `loot` weights.
/// @param _initializationParams Encoded setup information.
function setUp(bytes memory _initializationParams) public initializer {
(
string memory _name, /*_name Name for erc20 `shares` accounting*/
string memory _symbol, /*_symbol Symbol for erc20 `shares` accounting*/
address _multisendLibrary, /*address of multisend library*/
bytes memory _initializationMultisendData /*here you call BaalOnly functions to set up initial shares, loot, shamans, periods, etc.*/
) = abi.decode(_initializationParams, (string, string, address, bytes));
name = _name; /*initialize Baal `name` with erc20 accounting*/
symbol = _symbol; /*initialize Baal `symbol` with erc20 accounting*/
multisendLibrary = _multisendLibrary;
// Execute all setups including
// * mint shares
// * convert shares to loot
// * set shamans
// * set periods
require(
execute(
multisendLibrary,
0,
_initializationMultisendData,
Enum.Operation.DelegateCall,
gasleft()
),
"call failure"
);
status = 1; /*initialize 'reentrancy guard' status*/
}
/// @notice Delegates Baal voting weight only on initialization for summoners.
function delegateSummoners(
address[] memory _delegators,
address[] memory _delegatees
) external initializer baalOnly {
for (uint256 i; i < _delegators.length; i++) {
_delegate(_delegators[i], _delegatees[i]); /*delegate `summoners` voting weights to themselves - this saves a step before voting*/
}
}
/// @notice Execute membership action to mint or burn `shares` and/or `loot` against whitelisted `shamans` in consideration of user & given amounts.
/// @param shaman Whitelisted contract to trigger action.
/// @param loot Economic weight involved in external call.
/// @param shares Voting weight involved in external call.
/// @param mint Confirm whether action involves 'mint' or 'burn' action - if `false`, perform burn.
/// @return lootOut sharesOut Membership updates derived from action.
function memberAction(
address shaman,
uint96 loot,
uint96 shares,
bool mint
) external payable nonReentrant returns (uint96 lootOut, uint96 sharesOut) {
require(shamans[shaman], "!shaman"); /*check `shaman` is approved*/
(lootOut, sharesOut) = IShaman(shaman).memberAction{value: msg.value}(
msg.sender,
loot,
shares
); /*fetch 'reaction' per inputs*/
if (mint) {
/*execute `mint` actions*/
if (lootOut != 0) _mintLoot(msg.sender, lootOut); /*add `loot` to user account & Baal total*/
if (sharesOut != 0) _mintShares(msg.sender, sharesOut); /*add `shares` to user account & Baal total with erc20 accounting*/
} else {
/*otherwise, execute `burn` actions*/
if (lootOut != 0) _burnLoot(msg.sender, lootOut); /*subtract `loot` from user account & Baal total*/
if (sharesOut != 0) _burnShares(msg.sender, sharesOut); /*subtract `shares` from user account & Baal total with erc20 accounting*/
}
}
/*****************
PROPOSAL FUNCTIONS
*****************/
/// @notice Submit proposal to Baal `members` for approval within given voting period.
/// @param votingPeriod Voting period in seconds.
/// @param proposalData Multisend encoded transactions or proposal data
/// @param details Context for proposal.
/// @return proposal Count for submitted proposal.
function submitProposal(
uint32 votingPeriod,
bytes calldata proposalData,
string calldata details
) external nonReentrant returns (uint256 proposal) {
require(
minVotingPeriod <= votingPeriod && votingPeriod <= maxVotingPeriod,
"!votingPeriod"
); /*check voting period is within Baal bounds*/
bool selfSponsor; /*plant sponsor flag*/
if (balanceOf[msg.sender] != 0) selfSponsor = true; /*if a member, self-sponsor*/
unchecked {
proposalCount++; /*increment proposal counter*/
proposals[proposalCount] = Proposal( /*push params into proposal struct - start voting period timer if member submission*/
votingPeriod,
selfSponsor ? uint32(block.timestamp) : 0,
selfSponsor ? uint32(block.timestamp) + votingPeriod : 0,
0,
0,
proposalData,
details
);
}
emit SubmitProposal(proposal, votingPeriod, proposalData, details); /*emit event reflecting proposal submission*/
}
/// @notice Sponsor proposal to Baal `members` for approval within voting period.
/// @param proposal Number of proposal in `proposals` mapping to sponsor.
function sponsorProposal(uint256 proposal) external nonReentrant {
Proposal storage prop = proposals[proposal]; /*alias proposal storage pointers*/
require(balanceOf[msg.sender] != 0, "!member"); /*check 'membership' - required to sponsor proposal*/
require(prop.votingPeriod != 0, "!exist"); /*check proposal existence*/
require(prop.votingStarts == 0, "sponsored"); /*check proposal not already sponsored*/
prop.votingStarts = uint32(block.timestamp);
unchecked {
prop.votingEnds = uint32(block.timestamp) + prop.votingPeriod;
}
emit SponsorProposal(msg.sender, proposal, block.timestamp);
}
/// @notice Submit vote - proposal must exist & voting period must not have ended.
/// @param proposal Number of proposal in `proposals` mapping to cast vote on.
/// @param approved If 'true', member will cast `yesVotes` onto proposal - if 'false', `noVotes` will be counted.
function submitVote(uint256 proposal, bool approved) external nonReentrant {
Proposal storage prop = proposals[proposal]; /*alias proposal storage pointers*/
uint96 balance = getPriorVotes(msg.sender, prop.votingStarts); /*fetch & gas-optimize voting weight at proposal creation time*/
require(prop.votingEnds >= block.timestamp, "ended"); /*check voting period has not ended*/
require(!members[msg.sender].voted[proposal], "voted"); /*check vote not already cast*/
unchecked {
if (approved) {
/*if `approved`, cast delegated balance `yesVotes` to proposal*/
prop.yesVotes += balance;
members[msg.sender].highestIndexYesVote = proposal;
} else {
/*otherwise, cast delegated balance `noVotes` to proposal*/
prop.noVotes += balance;
}
}
members[msg.sender].voted[proposal] = true; /*record voting action to `members` struct per user account*/
emit SubmitVote(msg.sender, balance, proposal, approved); /*emit event reflecting vote*/
}
/// @notice Submit vote with EIP-712 signature - proposal must exist & voting period must not have ended.
/// @param proposal Number of proposal in `proposals` mapping to cast vote on.
/// @param approved If 'true', member will cast `yesVotes` onto proposal - if 'false', `noVotes` will be counted.
/// @param v The recovery byte of the signature.
/// @param r Half of the ECDSA signature pair.
/// @param s Half of the ECDSA signature pair.
function submitVoteWithSig(
uint256 proposal,
bool approved,
uint8 v,
bytes32 r,
bytes32 s
) external nonReentrant {
Proposal storage prop = proposals[proposal]; /*alias proposal storage pointers*/
bytes32 domainSeparator = keccak256(
abi.encode(
DOMAIN_TYPEHASH,
keccak256(bytes(name)),
block.chainid,
address(this)
)
); /*calculate EIP-712 domain hash*/
bytes32 structHash = keccak256(
abi.encode(VOTE_TYPEHASH, proposal, approved)
); /*calculate EIP-712 struct hash*/
bytes32 digest = keccak256(
abi.encodePacked("\x19\x01", domainSeparator, structHash)
); /*calculate EIP-712 digest for signature*/
address signatory = ecrecover(digest, v, r, s); /*recover signer from hash data*/
require(signatory != address(0), "!signatory"); /*check signer is not null*/
uint96 balance = getPriorVotes(signatory, prop.votingStarts); /*fetch & gas-optimize voting weight at proposal creation time*/
require(prop.votingEnds >= block.timestamp, "ended"); /*check voting period has not ended*/
require(!members[signatory].voted[proposal], "voted"); /*check vote not already cast*/
unchecked {
if (approved) {
/*if `approved`, cast delegated balance `yesVotes` to proposal*/
prop.yesVotes += balance;
members[signatory].highestIndexYesVote = proposal;
} else {
/*otherwise, cast delegated balance `noVotes` to proposal*/
prop.noVotes += balance;
}
}
members[signatory].voted[proposal] = true; /*record voting action to `members` struct per user account*/
emit SubmitVote(signatory, balance, proposal, approved); /*emit event reflecting vote*/
}
// ********************
// PROCESSING FUNCTIONS
// ********************
/// @notice Process `proposal` & execute internal functions.
/// @param proposal Number of proposal in `proposals` mapping to process for execution.
function processProposal(uint256 proposal) external nonReentrant {
Proposal storage prop = proposals[proposal]; /*alias `proposal` storage pointers*/
_processingReady(proposal, prop); /*validate `proposal` processing requirements*/
/*check if `proposal` approved by simple majority of members*/
if (prop.yesVotes > prop.noVotes) {
proposalsPassed[proposal] = true; /*flag that proposal passed - allows minion-like extensions*/
processActionProposal(prop); /*execute 'action'*/
}
delete proposals[proposal]; /*delete given proposal struct details for gas refund & the commons*/
emit ProcessProposal(proposal); /*emit event reflecting that given proposal processed*/
}
/// @notice Internal function to process 'action' proposal.
function processActionProposal(Proposal memory prop) private {
require(
execute(
multisendLibrary,
0,
prop.proposalData,
Enum.Operation.DelegateCall,
gasleft()
),
"call failure"
);
}
/// @notice Baal-only function to mint shares.
function mintShares(address[] calldata to, uint96[] calldata amount)
external
baalOnly
{
require(to.length == amount.length, "!array parity"); /*check array lengths match*/
for (uint256 i = 0; i < to.length; i++) {
_mintShares(to[i], amount[i]); /*grant `to` `amount` `shares`*/
}
}
/// @notice Baal-only function to mint loot.
function mintLoot(address[] calldata to, uint96[] calldata amount)
external
baalOnly
{
require(to.length == amount.length, "!array parity"); /*check array lengths match*/
for (uint256 i = 0; i < to.length; i++) {
_mintLoot(to[i], amount[i]); /*grant `to` `amount` `shares`*/
}
}
/// @notice Baal-only function to convert shares to loot.
function convertSharesToLoot(address to) external baalOnly {
uint96 removedBalance = uint96(balanceOf[to]); /*gas-optimize variable*/
_burnShares(to, removedBalance); /*burn all of `to` `shares` & convert into `loot`*/
_mintLoot(to, removedBalance); /*mint equivalent `loot`*/
}
/// @notice Baal-only function to change periods.
function setPeriods(bytes memory _periodData) external baalOnly {
(
uint32 min,
uint32 max,
uint32 grace,
bool pauseLoot,
bool pauseShares
) = abi.decode(_periodData, (uint32, uint32, uint32, bool, bool));
if (min != 0) minVotingPeriod = min; /*if positive, reset min. voting periods to first `value`*/
if (max != 0) maxVotingPeriod = max; /*if positive, reset max. voting periods to second `value`*/
if (grace != 0) gracePeriod = grace; /*if positive, reset grace period to third `value`*/
lootPaused = pauseLoot; /*set pause `loot` transfers on fifth `value`*/
sharesPaused = pauseShares; /*set pause `shares` transfers on sixth `value`*/
}
/// @notice Baal-only function to set flash fee numerator.
function setFlashFeeNumerator(uint32 _flashFeeNumerator) external baalOnly {
flashFeeNumerator = _flashFeeNumerator;
}
/// @notice Baal-only function to set shaman status.
function setShamans(address[] calldata _shamans, bool enabled)
external
baalOnly
{
for (uint256 i; i < _shamans.length; i++) {
shamans[_shamans[i]] = enabled;
}
}
/// @notice Baal-only function to whitelist guildToken.
function setGuildTokens(address[] calldata _tokens) external baalOnly {
for (uint256 i; i < _tokens.length; i++) {
if (guildTokens.length != MAX_GUILD_TOKEN_COUNT)
guildTokens.push(_tokens[i]); /*push account to `guildTokens` array if within 'MAX'*/
}
}
/// @notice Baal-only function to remove guildToken
function unsetGuildTokens(uint256[] calldata _tokenIndexes)
external
baalOnly
{
for (uint256 i; i < _tokenIndexes.length; i++) {
guildTokens[_tokenIndexes[i]] = guildTokens[guildTokens.length - 1]; /*swap-to-delete index with last value*/
guildTokens.pop(); /*pop account from `guildTokens` array*/
}
}
/*******************
GUILD MGMT FUNCTIONS
*******************/
/// @notice Approve `to` to transfer up to `amount`.
/// @return success Whether or not the approval succeeded.
function approve(address to, uint256 amount)
external
returns (bool success)
{
allowance[msg.sender][to] = amount; /*adjust `allowance`*/
emit Approval(msg.sender, to, amount); /*emit event reflecting approval*/
success = true; /*confirm approval with ERC-20 accounting*/
}
/// @notice Delegate votes from user to `delegatee`.
/// @param delegatee The address to delegate votes to.
function delegate(address delegatee) external {
_delegate(msg.sender, delegatee);
}
/// @notice Delegates votes from `signatory` to `delegatee` with EIP-712 signature.
/// @param delegatee The address to delegate 'votes' to.
/// @param nonce The contract state required to match the signature.
/// @param deadline The time at which to expire the signature.
/// @param v The recovery byte of the signature.
/// @param r Half of the ECDSA signature pair.
/// @param s Half of the ECDSA signature pair.
function delegateBySig(
address delegatee,
uint256 nonce,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external {
bytes32 domainSeparator = keccak256(
abi.encode(
DOMAIN_TYPEHASH,
keccak256(bytes(name)),
block.chainid,
address(this)
)
); /*calculate EIP-712 domain hash*/
bytes32 structHash = keccak256(
abi.encode(DELEGATION_TYPEHASH, delegatee, nonce, deadline)
); /*calculate EIP-712 struct hash*/
bytes32 digest = keccak256(
abi.encodePacked("\x19\x01", domainSeparator, structHash)
); /*calculate EIP-712 digest for signature*/
address signatory = ecrecover(digest, v, r, s); /*recover signer from hash data*/
require(signatory != address(0), "!signatory"); /*check signer is not null*/
unchecked {
require(nonce == nonces[signatory]++, "!nonce"); /*check given `nonce` is next in `nonces`*/
}
require(block.timestamp <= deadline, "expired"); /*check signature is not expired*/
_delegate(signatory, delegatee); /*execute delegation*/
}
/// @notice Triggers an approval from `owner` to `spender` with EIP-712 signature.
/// @param owner The address to approve from.
/// @param spender The address to be approved.
/// @param amount The number of `shares` tokens that are approved (2^256-1 means infinite).
/// @param deadline The time at which to expire the signature.
/// @param v The recovery byte of the signature.
/// @param r Half of the ECDSA signature pair.
/// @param s Half of the ECDSA signature pair.
function permit(
address owner,
address spender,
uint96 amount,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external {
bytes32 domainSeparator = keccak256(
abi.encode(
DOMAIN_TYPEHASH,
keccak256(bytes(name)),
block.chainid,
address(this)
)
); /*calculate EIP-712 domain hash*/
unchecked {
bytes32 structHash = keccak256(
abi.encode(
PERMIT_TYPEHASH,
owner,
spender,
amount,
nonces[owner]++,
deadline
)
); /*calculate EIP-712 struct hash*/
bytes32 digest = keccak256(
abi.encodePacked("\x19\x01", domainSeparator, structHash)
); /*calculate EIP-712 digest for signature*/
address signatory = ecrecover(digest, v, r, s); /*recover signer from hash data*/
require(signatory != address(0), "!signatory"); /*check signer is not null*/
require(signatory == owner, "!authorized"); /*check signer is `owner`*/
}
require(block.timestamp <= deadline, "expired"); /*check signature is not expired*/
allowance[owner][spender] = amount; /*adjust `allowance`*/
emit Approval(owner, spender, amount); /*emit event reflecting approval*/
}
/// @notice Transfer `amount` tokens from user to `to`.
/// @param to The address of destination account.
/// @param amount The number of `shares` tokens to transfer.
/// @return success Whether or not the transfer succeeded.
function transfer(address to, uint96 amount)
external
returns (bool success)
{
require(!sharesPaused, "!transferable");
balanceOf[msg.sender] -= amount;
unchecked {
balanceOf[to] += amount;
}
_moveDelegates(delegates[msg.sender], delegates[to], amount);
emit Transfer(msg.sender, to, amount);
success = true;
}
/// @notice Transfer `amount` tokens from `from` to `to`.
/// @param from The address of the source account.
/// @param to The address of the destination account.
/// @param amount The number of `shares` tokens to transfer.
/// @return success Whether or not the transfer succeeded.
function transferFrom(
address from,
address to,
uint96 amount
) external returns (bool success) {
require(!sharesPaused, "!transferable");
if (allowance[from][msg.sender] != type(uint256).max) {
allowance[from][msg.sender] -= amount;
}
balanceOf[from] -= amount;
unchecked {
balanceOf[to] += amount;
}
_moveDelegates(delegates[from], delegates[to], amount);
emit Transfer(from, to, amount);
success = true;
}
/// @notice Transfer `amount` `loot` from user to `to`.
/// @param to The address of destination account.
/// @param amount The sum of loot to transfer.
function transferLoot(address to, uint96 amount) external {
require(!lootPaused, "!transferable");
members[msg.sender].loot -= amount;
unchecked {
members[to].loot += amount;
}
emit TransferLoot(msg.sender, to, amount);
}
/// @notice Flashloan ability that conforms to `IERC3156FlashLender`, as defined in 'https://eips.ethereum.org/EIPS/eip-3156'.
/// @param receiver Address of token receiver that conforms to `IERC3156FlashBorrower` & handles flashloan.
/// @param token The loan currency.
/// @param amount The amount of tokens lent.
/// @param data Arbitrary data structure, intended to contain user-defined parameters.
function flashLoan(
address receiver,
address token,
uint256 amount,
bytes calldata data
) external returns (bool success) {
uint256 fee = flashFee(token, amount);
require(fee != 0, "uninitialized");
_safeTransfer(token, receiver, amount);
(, bytes memory _flashData) = receiver.call(
abi.encodeWithSelector(
0x23e30c8b,
msg.sender,
token,
amount,
fee,
data
)
); /*'onFlashLoan(address,address,uint,uint,bytes)'*/
bytes32 flashData = abi.decode(_flashData, (bytes32));
require(
flashData == keccak256("ERC3156FlashBorrower.onFlashLoan"),
"Callback failed"
); /*checks flash loan success*/
_safeTransferFrom(token, receiver, address(this), amount + fee);
success = true;
}
/// @notice Process member burn of `shares` and/or `loot` to claim 'fair share' of `guildTokens`.
/// @param to Account that receives 'fair share'.
/// @param lootToBurn Baal pure economic weight to burn.
/// @param sharesToBurn Baal voting weight to burn.
function ragequit(
address to,
uint96 lootToBurn,
uint96 sharesToBurn
) external nonReentrant {
require(
proposals[members[msg.sender].highestIndexYesVote].votingEnds == 0,
"processed"
); /*check highest index proposal member approved has processed*/
for (uint256 i; i < guildTokens.length; i++) {
(, bytes memory balanceData) = guildTokens[i].staticcall(
abi.encodeWithSelector(0x70a08231, address(this))
); /*get Baal token balances - 'balanceOf(address)'*/
uint256 balance = abi.decode(balanceData, (uint256)); /*decode Baal token balances for calculation*/
uint256 amountToRagequit = ((lootToBurn + sharesToBurn) * balance) /
(totalSupply + totalLoot); /*calculate 'fair shair' claims*/
if (amountToRagequit != 0) {
/*gas optimization to allow higher maximum token limit*/
_safeTransfer(guildTokens[i], to, amountToRagequit); /*execute 'safe' token transfer*/
}
}
if (lootToBurn != 0) {
/*gas optimization*/
_burnLoot(msg.sender, lootToBurn); /*subtract `loot` from user account & Baal totals*/
}
if (sharesToBurn != 0) {
/*gas optimization*/
_burnShares(msg.sender, sharesToBurn); /*subtract `shares` from user account & Baal totals with erc20 accounting*/
}
emit Ragequit(msg.sender, to, lootToBurn, sharesToBurn); /*event reflects claims made against Baal*/
}
/***************
GETTER FUNCTIONS
***************/
/// @notice Returns the current delegated `vote` balance for `account`.
/// @param account The user to check delegated `votes` for.
/// @return votes Current `votes` delegated to `account`.
function getCurrentVotes(address account)
external
view
returns (uint96 votes)
{
uint256 nCheckpoints = numCheckpoints[account];
unchecked {
votes = nCheckpoints != 0
? checkpoints[account][nCheckpoints - 1].votes
: 0;
}
}
/// @notice Returns the prior number of `votes` for `account` as of `timeStamp`.
/// @param account The user to check `votes` for.
/// @param timeStamp The unix time to check `votes` for.
/// @return votes Prior `votes` delegated to `account`.
function getPriorVotes(address account, uint256 timeStamp)
public
view
returns (uint96 votes)
{
require(timeStamp < block.timestamp, "!determined");
uint256 nCheckpoints = numCheckpoints[account];
if (nCheckpoints == 0) return 0;
unchecked {
if (
checkpoints[account][nCheckpoints - 1].fromTimeStamp <=
timeStamp
) return checkpoints[account][nCheckpoints - 1].votes;
if (checkpoints[account][0].fromTimeStamp > timeStamp) return 0;
uint256 lower = 0;
uint256 upper = nCheckpoints - 1;
while (upper > lower) {
uint256 center = upper - (upper - lower) / 2;
Checkpoint memory cp = checkpoints[account][center];
if (cp.fromTimeStamp == timeStamp) return cp.votes;
else if (cp.fromTimeStamp < timeStamp) lower = center;
else upper = center - 1;
}
votes = checkpoints[account][lower].votes;
}
}
/// @notice Returns array list of approved `guildTokens` in Baal for {ragequit}.
/// @return tokens ERC-20s approved for {ragequit}.
function getGuildTokens() external view returns (address[] memory tokens) {
tokens = guildTokens;
}
/// @notice Returns the `fee` to be charged for a flash loan.
/// @param amount The sum of tokens lent.
/// @return fee The `fee` amount of 'token' to be charged for the loan, on top of the returned principal - uniform in Baal.
function flashFee(address, uint256 amount)
public
view
returns (uint256 fee)
{
fee = (amount * flashFeeNumerator) / 10000; /*Calculate `fee` - precision factor '10000' derived from ERC-3156 'Flash Loan Reference'*/
}
/// @notice Returns the `max` amount of `token` available to be lent.
/// @param token The loan currency.
/// @return max The `amount` of `token` that can be borrowed.
function maxFlashLoan(address token) external view returns (uint256 max) {
(, bytes memory balanceData) = token.staticcall(
abi.encodeWithSelector(0x70a08231, address(this))
); /*get Baal token balance - 'balanceOf(address)'*/
max = abi.decode(balanceData, (uint256)); /*decode Baal token balance for calculation*/
}
/***************
HELPER FUNCTIONS
***************/
/// @notice Allows batched calls to Baal.
/// @param data An array of payloads for each call.
function multicall(bytes[] calldata data)
external
returns (bytes[] memory results)
{
results = new bytes[](data.length);
unchecked {
for (uint256 i = 0; i < data.length; i++) {
(bool success, bytes memory result) = address(this)
.delegatecall(data[i]);
if (!success) {
if (result.length < 68) revert();
assembly {
result := add(result, 0x04)
}
revert(abi.decode(result, (string)));
}
results[i] = result;
}
}
}
/// @notice Returns confirmation for 'safe' ERC-721 (NFT) transfers to Baal.
function onERC721Received(
address,
address,
uint256,
bytes calldata
) external pure returns (bytes4 sig) {
sig = 0x150b7a02; /*'onERC721Received(address,address,uint,bytes)'*/
}
/// @notice Returns confirmation for 'safe' ERC-1155 transfers to Baal.
function onERC1155Received(
address,
address,
uint256,
uint256,
bytes calldata
) external pure returns (bytes4 sig) {
sig = 0xf23a6e61; /*'onERC1155Received(address,address,uint,uint,bytes)'*/
}
/// @notice Returns confirmation for 'safe' batch ERC-1155 transfers to Baal.
function onERC1155BatchReceived(
address,
address,
uint256[] calldata,
uint256[] calldata,
bytes calldata
) external pure returns (bytes4 sig) {
sig = 0xbc197c81; /*'onERC1155BatchReceived(address,address,uint[],uint[],bytes)'*/
}
/// @notice Deposits ETH sent to Baal.
receive() external payable {}
/// @notice Delegates Baal voting weight.
function _delegate(address delegator, address delegatee) private {
address currentDelegate = delegates[delegator];
delegates[delegator] = delegatee;
_moveDelegates(
currentDelegate,
delegatee,
uint96(balanceOf[delegator])
);
emit DelegateChanged(delegator, currentDelegate, delegatee);
}
/// @notice Elaborates delegate update - cf., 'Compound Governance'.
function _moveDelegates(
address srcRep,
address dstRep,
uint96 amount
) private {
unchecked {
if (srcRep != dstRep && amount != 0) {
if (srcRep != address(0)) {
uint256 srcRepNum = numCheckpoints[srcRep];
uint96 srcRepOld = srcRepNum != 0
? checkpoints[srcRep][srcRepNum - 1].votes
: 0;
uint96 srcRepNew = srcRepOld - amount;
_writeCheckpoint(srcRep, srcRepNum, srcRepOld, srcRepNew);
}
if (dstRep != address(0)) {
uint256 dstRepNum = numCheckpoints[dstRep];
uint96 dstRepOld = dstRepNum != 0
? checkpoints[dstRep][dstRepNum - 1].votes
: 0;
uint96 dstRepNew = dstRepOld + amount;
_writeCheckpoint(dstRep, dstRepNum, dstRepOld, dstRepNew);
}
}
}
}
/// @notice Elaborates delegate update - cf., 'Compound Governance'.
function _writeCheckpoint(
address delegatee,
uint256 nCheckpoints,
uint96 oldVotes,
uint96 newVotes
) private {
uint32 timeStamp = uint32(block.timestamp);
unchecked {
if (
nCheckpoints != 0 &&
checkpoints[delegatee][nCheckpoints - 1].fromTimeStamp ==
timeStamp
) {
checkpoints[delegatee][nCheckpoints - 1].votes = newVotes;
} else {
checkpoints[delegatee][nCheckpoints] = Checkpoint(
timeStamp,
newVotes
);
numCheckpoints[delegatee] = nCheckpoints + 1;
}
}
emit DelegateVotesChanged(delegatee, oldVotes, newVotes);
}
/// @notice Burn function for Baal `loot`.
function _burnLoot(address from, uint96 loot) private {
members[from].loot -= loot; /*subtract `loot` for `from` account*/
unchecked {
totalLoot -= loot; /*subtract from total Baal `loot`*/
}
emit TransferLoot(from, address(0), loot); /*emit event reflecting burn of `loot`*/
}
/// @notice Burn function for Baal `shares`.
function _burnShares(address from, uint96 shares) private {
balanceOf[from] -= shares; /*subtract `shares` for `from` account*/
unchecked {
totalSupply -= shares; /*subtract from total Baal `shares`*/
}
_moveDelegates(delegates[from], address(0), shares); /*update delegation*/
emit Transfer(from, address(0), shares); /*emit event reflecting burn of `shares` with erc20 accounting*/
}
/// @notice Minting function for Baal `loot`.
function _mintLoot(address to, uint96 loot) private {
unchecked {
if (totalSupply + loot <= type(uint96).max / 2) {
members[to].loot += loot; /*add `loot` for `to` account*/
totalLoot += loot; /*add to total Baal `loot`*/
emit TransferLoot(address(0), to, loot); /*emit event reflecting mint of `loot`*/
}
}
}
/// @notice Minting function for Baal `shares`.
function _mintShares(address to, uint96 shares) private {
unchecked {
if (totalSupply + shares <= type(uint96).max / 2) {
balanceOf[to] += shares; /*add `shares` for `to` account*/
totalSupply += shares; /*add to total Baal `shares`*/
_moveDelegates(address(0), delegates[to], shares); /*update delegation*/
emit Transfer(address(0), to, shares); /*emit event reflecting mint of `shares` with erc20 accounting*/
}
}
}
/// @notice Check to validate proposal processing requirements.
function _processingReady(uint256 proposal, Proposal memory prop)
private
view
returns (bool ready)
{
unchecked {
require(proposal <= proposalCount, "!exist"); /*check proposal exists*/
require(proposals[proposal - 1].votingEnds == 0, "prev!processed"); /*check previous proposal has processed by deletion*/
require(proposals[proposal].votingEnds != 0, "processed"); /*check given proposal has been sponsored & not yet processed by deletion*/
if (singleSummoner) return true; /*if single member, process early*/
if (prop.yesVotes > totalSupply / 2) return true; /*process early if majority member support*/
require(prop.votingEnds + gracePeriod <= block.timestamp, "!ended"); /*check voting period has ended*/
ready = true; /*otherwise, process if voting period done*/
}
}
/// @notice Provides 'safe' {transfer} for tokens that do not consistently return 'true/false'.
function _safeTransfer(
address token,
address to,
uint256 amount
) private {
(bool success, bytes memory data) = token.call(
abi.encodeWithSelector(0xa9059cbb, to, amount)
); /*'transfer(address,uint)'*/
require(
success && (data.length == 0 || abi.decode(data, (bool))),
"transfer failed"
); /*checks success & allows non-conforming transfers*/
}
/// @notice Provides 'safe' {transferFrom} for tokens that do not consistently return 'true/false'.
function _safeTransferFrom(
address token,
address from,
address to,
uint256 amount
) private {
(bool success, bytes memory data) = token.call(
abi.encodeWithSelector(0x23b872dd, from, to, amount)
); /*'transferFrom(address,address,uint)'*/
require(
success && (data.length == 0 || abi.decode(data, (bool))),
"transferFrom failed"
); /*checks success & allows non-conforming transfers*/
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment