sherlock-2023-11-nouns-builder-m02
[M-02] MerkleReserveMinter minting methodology is incompatible with current governance structure and can lead to migrated DAOs being hijacked immediately
Summary
MerkleReserveMinter는 대량의 토큰을 즉시 발행할 수 있어, 토큰이 개별적으로 경매를 통해 발행되어 표가 조금씩 증가하는 현재의 거버넌스 구조와 호환되지 않는다. 동일한 블록에서 제안을 발행하고 생성함으로써 사용자는 실제보다 낮은 정족수로 제안을 생성할 수 있다. 이를 통해 마이그레이션된 DAO를 더 쉽게 탈취할 수 있다.
Keyword
dao, 51% attack
Vulnerability
MerkleReserveMinter 에서 민팅할 때, 유저는 단일 트랜잭션에서 원하는 만큼(자신에게 할당된 내에서) 토큰을 발행받을 수 있다. 이는 단일 트랜잭션에서 토큰 공급이 급격하게 증가할 수 있음을 의미한다.
function mintFromReserve(address tokenContract, MerkleClaim[] calldata claims) public payable {
uint256 claimCount = claims.length;
...
unchecked {
@> for (uint256 i = 0; i < claimCount; ++i) {
// Load claim in memory
MerkleClaim memory claim = claims[I];
// Requires one proof per tokenId to handle cases where users want to partially claim
if (!MerkleProof.verify(claim.merkleProof, settings.merkleRoot, keccak256(abi.encode(claim.mintTo, claim.tokenId)))) {
revert INVALID_MERKLE_PROOF(claim.mintTo, claim.merkleProof, settings.merkleRoot);
}
// Only allowing reserved tokens to be minted for this strategy
@> IToken(tokenContract).mintFromReserveTo(claim.mintTo, claim.tokenId);
}
}
...
}DAO에 제안을 생성할 때, quorumVotes는 제안 시점까지의 totalSupply의 quorumThresholdBps % 로 설정된다. timeCreated는 현재 block.time으로 설정된다.
function propose(
address[] memory _targets,
uint256[] memory _values,
bytes[] memory _calldatas,
string memory _description
) external returns (bytes32) {
...
// Store the proposal data
proposal.voteStart = SafeCast.toUint32(snapshot);
proposal.voteEnd = SafeCast.toUint32(deadline);
proposal.proposalThreshold = SafeCast.toUint32(currentProposalThreshold);
@> proposal.quorumVotes = SafeCast.toUint32(quorum());
proposal.proposer = msg.sender;
@> proposal.timeCreated = SafeCast.toUint32(block.timestamp);
emit ProposalCreated(proposalId, _targets, _values, _calldatas, _description, descriptionHash, proposal);
return proposalId;
}
function quorum() public view returns (uint256) {
unchecked {
@> return (settings.token.totalSupply() * settings.quorumThresholdBps) / BPS_PER_100_PERCENT;
}
}투표를 할 때는 제안이 생성된 시점의 표를 이용한다. 즉, 제안을 만든 후 동일 트랜잭션에서 NFT를 발행한다면 투표 자격을 가질 수 있다.
function _castVote(
bytes32 _proposalId,
address _voter,
uint256 _support,
string memory _reason
) internal returns (uint256) {
...
unchecked {
// Get the voter's weight at the time the proposal was created
@> weight = getVotes(_voter, proposal.timeCreated);낮은 정족수로 제안을 작성한 뒤 동일 트랜잭션에서 토큰을 다량 발행받으면 쉽게 51% 공격을 할 수 있다.
Impact
DAO를 완전히 탈취할 수 있다.
Mitigation
체크포인트 기반의 totalSupply를 사용하도록 변경한다. 정족수는 현재 totalSupply 대신 이를 기반으로 계산한다.
실제로는 모든 예약된 토큰이 발행되었거나 지연 시간이 지난 후에만 제안을 생성할 수 있도록 수정했다.
tags: bughunting, nouns dao, smart contract, solidity, dao, 51% attack, severity medium