Awesome
Moloch
Moloch the incomprehensible prison! Moloch the crossbone soulless jailhouse and Congress of sorrows! Moloch whose buildings are judgment! Moloch the vast stone of war! Moloch the stunned governments!
Moloch whose mind is pure machinery! Moloch whose blood is running money! Moloch whose fingers are ten armies! Moloch whose breast is a cannibal dynamo! Moloch whose ear is a smoking tomb!
~ Allen Ginsberg, Howl
Moloch is a grant-making DAO / Guild and a radical experiment in voluntary incentive alignment to overcome the "tragedy of the commons". Our objective is to accelerate the development of public Ethereum infrastructure that many teams need but don't want to pay for on their own. By pooling our ETH and ERC20 tokens, teams building on Ethereum can collectively fund open-source work we decide is in our common interest.
This documentation will focus on the Moloch DAO system design and smart contracts. For a deeper explanation of the philosophy behind Moloch, please read the Slate Star Codex post, Meditations on Moloch, which served as inspiration.
Design Principles
In developing the Moloch DAO, we realized that the more Solidity we wrote, the greater the likelihood that we would lose everyone's money. In order to prioritize security, we took simplicity and elegance as our primary design principles. We consiously skipped many features, and the result is what we believe to be a Minimally Viable DAO.
Overview
Moloch is described by three smart contracts:
Moloch.sol
- Responsible for managing membership & voting rights, proposal submissions, voting, and processing proposals based on the outcomes of the votes.GuildBank.sol
- Responsible for managing Guild assets.LootToken.sol
- An ERC20 mintable and burnable token with a claim on assets held by the Guild Bank.
Moloch has two classes of native assets:
- Voting Shares are minted and assigned when a new member is accepted into the Guild and provide voting rights on new membership proposals. They are non-transferrable, but can be irreversibly redeemed on a 1-1 basis for Loot Tokens.
- Loot Tokens are also minted on a 1-1 basis with Voting Shares when a new member is accepted, but are only disbursed when a member redeems their Voting Shares. Loot Tokens are freely transferrable and can at any time be consumed to collect a proportional share of all tokens held by the Guild in the Guild Bank.
Moloch operates through the submission, voting on, and processing of a series of membership proposals. To combat spam, new membership proposals can only be submitted by existing members and require a ~$5,000 ETH deposit. Applicants who wish to join must find a Guild member to champion their proposal and have that member call submitProposal
on their behalf. The membership proposal includes the number of Voting Shares the applicant is requesting, and either the set of tokens the applicant is offering as tribute or a pledge that the applicant will complete some work that benefits the Guild.
All tokens offered as tribute are held in escrow by the Moloch.sol
contract until the proposal vote is completed and processed. If a proposal vote passes, the applicant becomes a member, the Voting Shares requested are minted and assigned to them, and their tribute tokens are deposited into the GuildBank.sol
contract. If a proposal vote is rejected, all tribute tokens are returned to the applicant. In either case, the $5,000 ETH deposit is returned to the member who submitted the proposal.
Proposals are voted on in the order they are submitted. The voting period for each proposal is 7 days. During the voting period, members can vote (only once, no redos) on a proposal by calling submitVote
. A new proposal vote starts every day, so there can be a maximum of 7 proposals being voted on at any time (staggered by 1 day). Proposal votes are determined by simple majority, with a 50% quorum requirement.
At the end of the voting period, proposals enter into a 7 day grace period before the proposal is processed. During the grace period, all Guild members who voted No or didn't vote have the opportunity to ragequit, turning in their Voting Shares for Loot Tokens by calling collectLootTokens
and withdrawing their proportional share of tokens from the Guild Bank by calling redeemLootTokens
. Members who voted Yes must remain.
At the end of the grace period, proposals are processed when anyone calls processProposal
and are either accepted or rejected based on the votes of the remaining Guild members.
Game Theory
By allowing Guild members to ragequit and exit at any time, Moloch protects its members from 51% attacks and from supporting proposals they vehemently oppose.
In the worst case, one or more Guild members who control >50% of the Voting Shares could submit a proposal to grant themselves a ridiculous number of new Voting Shares, thereby diluting all other members of their claims to the Guild Bank assets and effectively stealing from them. If this were to happen, everyone else would ragequit during the grace period and take their share of the Guild Bank assets with them, and the proposal would have no impact.
In the more likely case of a contentious vote, those who oppose strongly enough can leave and increase the funding burden on those who choose to stay. Let's say the Guild has 100 outstanding Voting Shares and $100M worth of tokens in the Guild Bank. If a project proposal requests 1 newly minted Voting Share (~$1M worth), the vote is split 50/50 with 100% voter turnout, and the 50 who voted No all ragequit and take their $50M with them, then the remaining members would be diluting themselves twice as much: 1/51 = ~2% vs. 1/101 = ~1%.
In this fashion, the ragequit mechanism also provides an interesting incentive in favor of Guild cohesion. Guild members are disincentivized from voting Yes on proposals that they believe will make other members ragequit. Those who do vote Yes on contentious proposals will be forced to additionally dilute themselves proportional to the fraction of Voting Shares that ragequit in response.
The maximum additional dilution would be 4x, in the case of a proposal vote with 50% voter turnout (the quorum minimum) and just over 25% voting Yes and just under 25% voting No, where the 25% who voted No and the 50% who didn't vote all ragequit.
Moloch.sol
Data Structures
Global Constants
uint256 public periodDuration; // default = 86400 = 1 day in seconds
uint256 public votingPeriodLength; // default = 7 periods
uint256 public gracePeriodLength; // default = 7 periods
uint256 public proposalDeposit; // default = $5,000 worth of ETH at contract deployment (units in Wei)
GuildBank public guildBank; // guild bank contract reference
LootToken public lootToken; // loot token contract reference
uint8 constant QUORUM_NUMERATOR = 1;
uint8 constant QUORUM_DENOMINATOR = 2;
Internal Accounting
uint256 public currentPeriod = 0; // the current period number
uint256 public pendingProposals = 0; // the # of proposals waiting to be voted on
uint256 public totalVotingShares = 0; // total voting shares across all members
Proposals
The Proposal
struct stores all relevant data for each proposal, and is saved in the proposalQueue
array in the order it was submitted.
struct Proposal {
address proposer; // the member who submitted the proposal
address applicant; // the applicant who wishes to become a member
uint256 votingSharesRequested; // the # of voting shares the applicant is requesting
uint256 startingPeriod; // the period in which voting can start for this proposal
uint256 yesVotes; // the total number of YES votes for this proposal
uint256 noVotes; // the total number of NO votes for this proposal
bool processed; // true only if the proposal has been processed
address[] tributeTokenAddresses; // the addresses of the tokens the applicant has offered as tribute
uint256[] tributeTokenAmounts; // the amounts of the tokens the applicant has offered as tribute
mapping (address => Vote) votesByMember; // the votes on this proposal by each member
}
Proposal[] public proposalQueue;
Members
The Member
struct stores all relevant data for each member, and is saved in the members
mapping by the member's address.
struct Member {
address delegateKey; // the key responsible for submitting proposals and voting - defaults to member address unless updated
uint256 votingShares; // the # of voting shares assigned to this member
bool isActive; // always true once a member has been created
mapping (uint256 => Vote) votesByProposal; // records a member's votes by the index of the proposal
}
mapping (address => Member) public members;
mapping (address => address) public memberAddressByDelegateKey;
The isActive
field is set to true
when a member is accepted and remains true
even if a member redeems 100% of their Voting Shares. It is used to prevent overwriting existing members with new membership proposals.
For additional security, members can optionally change their delegateKey
(used for submitting and voting on proposals) to a different address by calling updateDelegateKey
. The memberAddressByDelegateKey
stores the member's address by the delegateKey
address.
Votes
The Vote enum reflects the possible values of a proposal vote by a member.
enum Vote {
Null, // default value, counted as abstention
Yes,
No
}
Periods
The Period
struct stores the start and end time for each period, and is saved in the periods
mapping by the period number.
struct Period {
uint256 startTime; // the starting unix timestamp in seconds
uint256 endTime; // the ending unix timestamp in seconds
}
mapping (uint256 => Period) public periods;
Modifiers
onlyMember
Checks that the msg.sender
is the address of a member with at least 1 voting share.
modifier onlyMember {
require(members[msg.sender].votingShares > 0, "Moloch::onlyMember - not a member");
_;
}
Applied only to collectLootTokens
and updateDelegateKey
.
onlyMemberDelegate
Checks that the msg.sender
is the delegateKey
of a member with at least 1 voting share.
modifier onlyMemberDelegate {
require(members[memberAddressByDelegateKey[msg.sender]].votingShares > 0, "Moloch::onlyMemberDelegate - not a member");
_;
}
Applied only to submitProposal
and submitVote
.
Functions
Moloch Constructor
- Deploys a new instance of the
LootToken.sol
contract and saves its reference. - Builds and saves the
GuildBank.sol
contract reference from the passed inguildBankAddress
. - Saves passed in values for global constants
periodDuration
,votingPeriodLength
,gracePeriodLength
, andproposalDeposit
. - Immediately starts the first period (period 0) at
startTime = now
. - Sets the
endTime
of the first period to 1 day fromnow
. - Initializes the voting shares of the founding members.
constructor(
address guildBankAddress,
address[] foundersAddresses,
uint256[] foundersVotingShares,
uint256 _periodDuration,
uint256 _votingPeriodLength,
uint256 _gracePeriodLength,
uint _proposalDeposit
)
public
{
lootToken = new LootToken();
guildBank = GuildBank(guildBankAddress);
periodDuration = _periodDuration;
votingPeriodLength = _votingPeriodLength;
gracePeriodLength = _gracePeriodLength;
proposalDeposit = _proposalDeposit;
uint256 startTime = now;
periods[currentPeriod].startTime = startTime;
periods[currentPeriod].endTime = startTime.add(periodDuration);
_addFoundingMembers(foundersAddresses, foundersVotingShares);
}
_addFoundingMembers
Only ever called once, from the constructor. For each founding member:
- Saves the founder's voting shares.
- Saves the founder's
delegateKey
as their founder address by default. - Updates the
totalVotingShares
. - Mints
lootTokens
equal the the voting shares (keeps them in theMoloch.sol
contract).
function _addFoundingMembers(
address[] membersArray,
uint256[] sharesArray
)
internal
{
require(membersArray.length == sharesArray.length, "Moloch::_addFoundingMembers - Provided arrays should match up.");
for (uint i = 0; i < membersArray.length; i++) {
address founder = membersArray[i];
uint256 shares = sharesArray[i];
require(shares > 0, "Moloch::_addFoundingMembers - founding member has 0 shares");
require(!members[founder].isActive, "Moloch::_addFoundingMembers - duplicate founder");
// use the founder address as the delegateKey by default
members[founder] = Member(founder, shares, true);
memberAddressByDelegateKey[founder] = founder;
totalVotingShares = totalVotingShares.add(shares);
lootToken.mint(this, shares);
}
}
updatePeriod
In order to make sure all interactions with Moloch take place during the correct period, the updatePeriod
function is called at the beginning of every state-updating function.
So long as the current time (now
) is greater than the endTime
of the current period (meaning the period is over), the currentPeriod
is incremented by one and then the startTime
and the endTime
for the next Period
are set as well.
When the currentPeriod
is incremented, if there are still pending proposals in the queue then pendingProposals
is decremented.
function updatePeriod() public {
while (now >= periods[currentPeriod].endTime) {
Period memory prevPeriod = periods[currentPeriod];
currentPeriod += 1;
periods[currentPeriod].startTime = prevPeriod.endTime;
periods[currentPeriod].endTime = prevPeriod.endTime.add(periodDuration);
if (pendingProposals > 0) {
pendingProposals = pendingProposals.sub(1);
}
}
}
The reason this is done using a while
loop is just in case an entire period passes without any Moloch interactions taking place.
submitProposal
At any time, members can submit new proposals using their delegateKey
.
- Updates the period.
- Transfers all tribute tokens to the
Moloch.sol
contract to be held in escrow until the proposal vote is completed and processed. - Sets the
startingPeriod
of the proposal. - Pushes the proposal to the end of the
proposalQueue
.
function submitProposal(
address applicant,
address[] tributeTokenAddresses,
uint256[] tributeTokenAmounts,
uint256 votingSharesRequested
)
public
payable
onlyMemberDelegate
{
updatePeriod();
address memberAddress = memberAddressByDelegateKey[msg.sender];
require(memberAddress == applicant || !members[applicant].isActive, "Moloch::submitProposal - applicant is an active member besides the proposer");
require(msg.value == proposalDeposit, "Moloch::submitProposal - insufficient proposalDeposit");
require(votingSharesRequested > 0, "Moloch::submitProposal - votingSharesRequested is zero");
for (uint256 i = 0; i < tributeTokenAddresses.length; i++) {
ERC20 token = ERC20(tributeTokenAddresses[i]);
uint256 amount = tributeTokenAmounts[i];
require(amount > 0, "Moloch::submitProposal - token tribute amount is 0");
require(token.transferFrom(applicant, this, amount), "Moloch::submitProposal - tribute token transfer failed");
}
pendingProposals = pendingProposals.add(1);
uint256 startingPeriod = currentPeriod + pendingProposals;
Proposal memory proposal = Proposal(memberAddress, applicant, votingSharesRequested, startingPeriod, 0, 0, tributeTokenAddresses, tributeTokenAmounts, false);
proposalQueue.push(proposal);
}
The startingPeriod
is set based on the currentPeriod
, and the number of pendingProposals
in queue before this one. If there are no pending proposals, then the starting period will be set to the next period. If there are pending proposals, the starting period is delayed by the number of pending proposals.
Existing members can earn additional voting shares through new proposals by
listing themselves as the applicant
, but must call submitProposal
themselves to do so.
submitVote
While a proposal is in its voting period, members can submit their vote using their delegateKey
.
- Updates the period.
- Saves the vote on the proposal struct by the member address.
- Saves the vote on the member struct by the proposal index.
- Based on their vote, adds the member's voting shares to the proposal
yesVotes
ornoVotes
tallies.
function submitVote(uint256 proposalIndex, uint8 uintVote) public onlyMemberDelegate {
updatePeriod();
address memberAddress = memberAddressByDelegateKey[msg.sender];
Proposal storage proposal = proposalQueue[proposalIndex];
Vote vote = Vote(uintVote);
require(proposal.startingPeriod > 0, "Moloch::submitVote - proposal does not exist");
require(currentPeriod >= proposal.startingPeriod, "Moloch::submitVote - voting period has not started");
require(currentPeriod.sub(proposal.startingPeriod) < votingPeriodLength, "Moloch::submitVote - proposal voting period has expired");
require(proposal.votesByMember[memberAddress] == Vote.Null, "Moloch::submitVote - member has already voted on this proposal");
require(vote == Vote.Yes || vote == Vote.No, "Moloch::submitVote - vote must be either Yes or No");
proposal.votesByMember[memberAddress] = vote;
Member storage member = members[memberAddress];
member.votesByProposal[proposalIndex] = vote;
if (vote == Vote.Yes) {
proposal.yesVotes.add(member.votingShares);
} else if (vote == Vote.No) {
proposal.noVotes.add(member.votingShares);
}
}
processProposal
After a proposal has completed its grace period, anyone can call processProposal
to tally the votes and either accept or reject it.
- Updates the period.
- Sets
proposal.processed = true
to prevent duplicate processing. - If quorum was reached and the vote passed:
3.1. If the member was applying on their own behalf, add the requested voting shares to their existing voting shares, and update any votes on active proposals in the voting or grace periods to reflect their new voting power.
3.2. If the applicant is a new member, save their data and set their default
delegateKey
to be the same as their member address. 3.3. Update thetotalVotingShares
. 3.4. MintslootTokens
equal the the voting shares (keeps them in theMoloch.sol
contract). 3.5. Transfer the tribute tokens being held in escrow to theGuildBank.sol
contract. - Otherwise: 4.1. Return all the tribute tokens being held in escrow to the applicant.
- Finally, return the $5,000 ether
proposalDeposit
.
function processProposal(uint256 proposalIndex) public {
updatePeriod();
Proposal storage proposal = proposalQueue[proposalIndex];
require(proposal.startingPeriod > 0, "Moloch::processProposal - proposal does not exist");
require(currentPeriod.sub(proposal.startingPeriod) > votingPeriodLength.add(gracePeriodLength), "Moloch::processProposal - proposal is not ready to be processed");
require(proposal.processed == false, "Moloch::processProposal - proposal has already been processed");
proposal.processed = true;
uint256 i = 0;
if (proposal.yesVotes.add(proposal.noVotes) >= (totalVotingShares.mul(QUORUM_NUMERATOR)).div(QUORUM_DENOMINATOR) && proposal.yesVotes > proposal.noVotes) {
// if the proposer is the applicant, add to their existing voting shares
if (proposal.proposer == proposal.applicant) {
members[proposal.applicant].votingShares = members[proposal.applicant].votingShares.add(proposal.votingSharesRequested);
// loop over their active proposal votes and add the new voting shares to any YES or NO votes
uint256 currentProposalIndex = proposalQueue.length.sub(pendingProposals.add(1));
uint256 oldestActiveProposal = (currentProposalIndex.sub(votingPeriodLength)).sub(gracePeriodLength);
for (uint256 i = currentProposalIndex; i > oldestActiveProposal; i--) {
if (isActiveProposal(i)) {
Proposal storage proposal = proposalQueue[i];
Vote vote = member.votesByProposal[i];
if (vote == Vote.Null) {
// member didn't vote on this proposal, skip to the next one
continue;
} else if (vote == Vote.Yes) {
proposal.yesVotes = proposal.yesVotes.add(proposal.votingSharesRequested);
} else {
proposal.noVotes = proposal.noVotes.add(proposal.votingSharesRequested);
}
} else {
// reached inactive proposal, exit the loop
break;
}
}
// the applicant is a new member, create a new record for them
} else {
// use applicant address as delegateKey by default
members[proposal.applicant] = Member(proposal.applicant, proposal.votingSharesRequested, true);
memberAddressByDelegateKey[proposal.applicant] = proposal.applicant;
}
// mint new voting shares and loot tokens
totalVotingShares = totalVotingShares.add(proposal.votingSharesRequested);
lootToken.mint(this, proposal.votingSharesRequested);
// deposit all tribute tokens to guild bank
for (i; i < proposal.tributeTokenAddresses.length; i++) {
require(guildBank.depositTributeTokens(this, proposal.tributeTokenAddresses[i], proposal.tributeTokenAmounts[i]));
}
} else {
// return all tokens
for (i; i < proposal.tributeTokenAddresses.length; i++) {
ERC20 token = ERC20(proposal.tributeTokenAddresses[i]);
require(token.transfer(proposal.applicant, proposal.tributeTokenAmounts[i]));
}
}
proposal.proposer.transfer(proposalDeposit);
}
collectLootTokens
At any time, so long as a member has not voted YES on any active proposals in the voting or grace periods, they can irreversibly redeem their voting shares for loot tokens.
- Update the period.
- Reduce the member's voting shares by the
lootAmount
being collected. - Reduce the total voting shares by the
lootAmount
. - Transfer
lootAmount
of loot tokens to the providedtreasury
address. - Update any active NO votes to reflect the member's new voting power.
function collectLootTokens(address treasury, uint256 lootAmount) public onlyMember {
updatePeriod();
Member storage member = members[msg.sender];
require(member.votingShares >= lootAmount, "Moloch::collectLoot - insufficient voting shares");
member.votingShares = member.votingShares.sub(lootAmount);
totalVotingShares = totalVotingShares.sub(lootAmount);
require(lootToken.transfer(treasury, lootAmount), "Moloch::collectLoot - loot token transfer failure");
// loop over their active proposal votes:
// - make sure they haven't voted YES on any active proposals
// - update any active NO votes to reflect their new voting power.
uint256 currentProposalIndex = proposalQueue.length.sub(pendingProposals.add(1));
uint256 oldestActiveProposal = (currentProposalIndex.sub(votingPeriodLength)).sub(gracePeriodLength);
for (uint256 i = currentProposalIndex; i > oldestActiveProposal; i--) {
if (isActiveProposal(i)) {
Proposal storage proposal = proposalQueue[i];
Vote vote = member.votesByProposal[i];
require(vote != Vote.Yes, "Moloch::collectLoot - member voted YES on active proposal");
if (vote == Vote.Null) {
// member didn't vote on this proposal, skip to the next one
continue;
}
// member voted No, revert the vote.
proposal.noVotes = proposal.noVotes.sub(lootAmount);
// if the member is collecting 100% of their loot, erase these vote completely
if (lootAmount == member.votingShares) {
proposal.votesByMember[msg.sender] = Vote.Null;
member.votesByProposal[i] = Vote.Null;
}
} else {
// reached inactive proposal, exit the loop
break;
}
}
}
updateDelegateKey
By default, when a member is accepted their delegateKey
is set to their member
address. At any time, they can change it to be any address that isn't already in
use, or back to their member address.
- Resets the old
delegateKey
reference in thememberAddressByDelegateKey
mapping. - Sets the reference for the new
delegateKey
to the member in thememberAddressByDelegateKey
mapping. - Updates the
member.delegateKey
.
function updateDelegateKey(address newDelegateKey) public onlyMember {
// newDelegateKey must be either the member's address or one not in use by any other members
require(newDelegateKey == msg.sender || !members[memberAddressByDelegateKey[msg.sender]].isActive);
Member storage member = members[msg.sender];
memberAddressByDelegateKey[member.delegateKey] = address(0);
memberAddressByDelegateKey[newDelegateKey] = msg.sender;
member.delegateKey = newDelegateKey;
}
isActiveProposal
A proposal is considered active if it is either in the voting or grace period.
// returns true if proposal is either in voting or grace period
function isActiveProposal(uint256 proposalIndex) internal view returns (bool) {
uint256 startingPeriod = proposalQueue[proposalIndex].startingPeriod;
return (currentPeriod >= startingPeriod && currentPeriod.sub(startingPeriod) < votingPeriodLength.add(gracePeriodLength));
}
GuildBank.sol
Data Structures
LootToken public lootToken; // loot token contract reference
mapping (address => bool) knownTokens; // true for tokens that have ever
been deposited into the guild back
address[] public tokenAddresses; // the complete set of unique token
addresses held by guild bank
mapping (uint256 => mapping (address => bool)) safeRedeemsById; // tracks
token addresses already withdrawn for each unique safeRedeem attempt to
prevent double-withdrawals
uint256 safeRedeemId = 0; // incremented on every safeRedeem attempt
Functions
setLootTokenAddress
Called only once immediately after contract deployment by the owner. Updates the
lootToken
address to point to the deployed LootToken.sol
contract.
Immediately after calling this function, as part of the migration script, the
owner will call transferOwnership
and make the Moloch.sol
contract the
permanent owner.
function setLootTokenAddress(address lootTokenAddress) public onlyOwner returns (address) {
require (address(lootTokenAddress) != address(0), "GuildBank::setLootTokenAddress address must not be zero");
require (address(lootToken) == address(0),"GuildBank::setLootTokenAddress Loot Token address already set");
lootToken = LootToken(lootTokenAddress);
return lootTokenAddress;
}
depositTributeTokens
Is called by the owner - the Moloch.sol
contract - in the processProposal
function.
- If this is the first token of it's kind being deposited, save its address in the
knownTokens
mapping and push its address to thetokenAddresses
array. - Transfers the admitted member's escrowed tribute tokens from Moloch to the Guild Bank.
function depositTributeTokens(
address sender,
address tokenAddress,
uint256 tokenAmount
) public onlyOwner returns (bool) {
if ((knownTokens[tokenAddress] == false) && (tokenAddress != address(lootToken))) {
knownTokens[tokenAddress] = true;
tokenAddresses.push(tokenAddress);
}
ERC20 token = ERC20(tokenAddress);
return (token.transferFrom(sender, this, tokenAmount));
}
redeemLootTokens
Can be used by anyone to consume their loot tokens and withdraw a proportional share of all tokens held by the guild bank.
- Transfer
lootAmount
of loot tokens from themsg.sender
to the guild bank. - Burn those loot tokens.
- Transfer a proportional share of all tokens held by the guild bank to the
provided
receiver
address.
function redeemLootTokens(
address receiver,
uint256 lootAmount
) public {
uint256 totalLootTokens = lootToken.totalSupply();
require(lootToken.transferFrom(msg.sender, this, lootAmount), "GuildBank::redeemLootTokens - lootToken transfer failed");
// burn lootTokens - will fail if approved lootToken balance is lower than lootAmount
lootToken.burn(lootAmount);
// transfer proportional share of all tokens held by the guild bank
for (uint256 i = 0; i < tokenAddresses.length; i++) {
ERC20 token = ERC20(tokenAddresses[i]);
uint256 tokenShare = token.balanceOf(this).mul(lootAmount).div(totalLootTokens);
require(token.transfer(receiver, tokenShare), "GuildBank::redeemLootTokens - token transfer failed");
}
}
safeRedeemLootTokens
If any of the tribute tokens held by the guild bank have transfer restrictions
that take effect, the redeemLootTokens
function above would fail. To
circumvent this, loot token holders can provide the set of token addresses they want to withdraw themselves, and skip any that would fail.
This function exists to be a safegaurd, not to give members a free pass. Guild members should all still be diligent to avoid accepting as tribute tokens that may introduce transfer restrictions.
- Increment the
safeRedeemId
- the unique id of eachsafeRedeemLootTokens
function call. - Transfer
lootAmount
of loot tokens from themsg.sender
to the guild bank. - Burn those loot tokens.
- For all unique tokens addresses in the provided
safeTokenAddresses
array, transfer a proportional share of the guild bank holdings to the providedreceiver
address.
function safeRedeemLootTokens(
address receiver,
uint256 lootAmount,
address[] safeTokenAddresses
) public {
safeRedeemId = safeRedeemId.add(1);
uint256 totalLootTokens = lootToken.totalSupply();
require(lootToken.transferFrom(msg.sender, this, lootAmount), "GuildBank::redeemLootTokens - lootToken transfer failed");
// burn lootTokens - will fail if approved lootToken balance is lower than lootAmount
lootToken.burn(lootAmount);
// transfer proportional share of all tokens held by the guild bank
for (uint256 i = 0; i < safeTokenAddresses.length; i++) {
if (!safeRedeemsById[safeRedeemId][safeTokenAddresses[i]]) {
safeRedeemsById[safeRedeemId][safeTokenAddresses[i]] = true;
ERC20 token = ERC20(safeTokenAddresses[i]);
uint256 tokenShare = token.balanceOf(this).mul(lootAmount).div(totalLootTokens);
require(token.transfer(receiver, tokenShare), "GuildBank::redeemLootTokens - token transfer failed");
}
}
}
The safeRedeemsById
tracks token addresses already withdrawn for each unique safeRedeemLootTokens
call to prevent double-withdrawals of the same token.