code-423n4 / 2021-09-yaxis-findings

0 stars 0 forks source link

Possibility to Stake Twice #21

Closed code423n4 closed 2 years ago

code423n4 commented 3 years ago

Handle

verifyfirst

Vulnerability details

Impact

Potential for some users to double-stake their Yaxis.

Proof of Concept

// SPDX-License-Identifier: MIT

pragma solidity 0.6.12;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol";

// yAxis (YAX) with Governance Alpha contract YaxisToken is ERC20 { using SafeERC20 for IERC20; using SafeMath for uint256;

address public governance;
uint256 public cap;
mapping(address => bool) public minters;

constructor (uint256 _cap) public ERC20("yAxis", "YAX") {
    governance = msg.sender;
    cap = _cap;
}

function mint(address _to, uint256 _amount) public {
    require(msg.sender == governance || minters[msg.sender], "!governance && !minter");
    _mint(_to, _amount);
    _moveDelegates(address(0), _delegates[_to], _amount);
}

function burn(uint256 _amount) public {
    _burn(msg.sender, _amount);
    _moveDelegates(_delegates[msg.sender], address(0), _amount);
}

function burnFrom(address _account, uint256 _amount) public {
    uint256 decreasedAllowance = allowance(_account, msg.sender).sub(_amount, "ERC20: burn amount exceeds allowance");
    _approve(_account, msg.sender, decreasedAllowance);
    _burn(_account, _amount);
    _moveDelegates(_delegates[_account], address(0), _amount);
}

function setGovernance(address _governance) public {
    require(msg.sender == governance, "!governance");
    governance = _governance;
}

function addMinter(address _minter) public {
    require(msg.sender == governance, "!governance");
    minters[_minter] = true;
}

function removeMinter(address _minter) public {
    require(msg.sender == governance, "!governance");
    minters[_minter] = false;
}

function setCap(uint256 _cap) public {
    require(msg.sender == governance, "!governance");
    require(_cap >= totalSupply(), "_cap is below current total supply");
    cap = _cap;
}

// This function allows governance to take unsupported tokens out of the contract.
// This is in an effort to make someone whole, should they seriously mess up.
// There is no guarantee governance will vote to return these.
// It also allows for removal of airdropped tokens.
function governanceRecoverUnsupported(IERC20 _token, address _to, uint256 _amount) external {
    require(msg.sender == governance, "!governance");
    _token.safeTransfer(_to, _amount);
}

/**
 * @dev See {ERC20-_beforeTokenTransfer}.
 *
 * Requirements:
 *
 * - minted tokens must not cause the total supply to go over the cap.
 */
function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual override {
    super._beforeTokenTransfer(from, to, amount);

    if (from == address(0)) {// When minting tokens
        require(totalSupply().add(amount) <= cap, "ERC20Capped: cap exceeded");
    }
}

// Copied and modified from YAM code:
// https://github.com/yam-finance/yam-protocol/blob/master/contracts/token/YAMGovernanceStorage.sol
// https://github.com/yam-finance/yam-protocol/blob/master/contracts/token/YAMGovernance.sol
// Which is copied and modified from COMPOUND:
// https://github.com/compound-finance/compound-protocol/blob/master/contracts/Governance/Comp.sol

/// @dev A record of each accounts delegate
mapping(address => address) internal _delegates;

/// @notice A checkpoint for marking number of votes from a given block
struct Checkpoint {
    uint32 fromBlock;
    uint256 votes;
}

/// @notice A record of votes checkpoints for each account, by index
mapping(address => mapping(uint32 => Checkpoint)) public checkpoints;

/// @notice The number of checkpoints for each account
mapping(address => uint32) public numCheckpoints;

/// @notice The EIP-712 typehash for the contract's domain
bytes32 public constant DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)");

/// @notice The EIP-712 typehash for the delegation struct used by the contract
bytes32 public constant DELEGATION_TYPEHASH = keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)");

/// @dev A record of states for signing / validating signatures
mapping(address => uint) public nonces;

/// @notice An event thats emitted when an account changes its delegate
event DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate);

/// @notice An event thats emitted when a delegate account's vote balance changes
event DelegateVotesChanged(address indexed delegate, uint previousBalance, uint newBalance);

/**
 * @notice Delegate votes from `msg.sender` to `delegatee`
 * @param delegator The address to get delegatee for
 */
function delegates(address delegator)
    external
    view
    returns (address)
{
    return _delegates[delegator];
}

/**
 * @notice Delegate votes from `msg.sender` to `delegatee`
 * @param delegatee The address to delegate votes to
 */
function delegate(address delegatee) external {
    return _delegate(msg.sender, delegatee);
}

/**
 * @notice Delegates votes from signatory to `delegatee`
 * @param delegatee The address to delegate votes to
 * @param nonce The contract state required to match the signature
 * @param expiry 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,
    uint nonce,
    uint expiry,
    uint8 v,
    bytes32 r,
    bytes32 s
)
    external
{
    bytes32 domainSeparator = keccak256(
        abi.encode(
            DOMAIN_TYPEHASH,
            keccak256(bytes(name())),
            getChainId(),
            address(this)
        )
    );

    bytes32 structHash = keccak256(
        abi.encode(
            DELEGATION_TYPEHASH,
            delegatee,
            nonce,
            expiry
        )
    );

    bytes32 digest = keccak256(
        abi.encodePacked(
            "\x19\x01",
            domainSeparator,
            structHash
        )
    );
    address signatory = ecrecover(digest, v, r, s);
    require(signatory != address(0), "YAX::delegateBySig: invalid signature");
    require(nonce == nonces[signatory]++, "YAX::delegateBySig: invalid nonce");
    require(now <= expiry, "YAX::delegateBySig: signature expired");
    return _delegate(signatory, delegatee);
}

/**
 * @notice Gets the current votes balance for `account`
 * @param account The address to get votes balance
 * @return The number of current votes for `account`
 */
function getCurrentVotes(address account)
    external
    view
    returns (uint256)
{
    uint32 nCheckpoints = numCheckpoints[account];
    return nCheckpoints > 0 ? checkpoints[account][nCheckpoints - 1].votes : 0;
}

/**
 * @notice Determine the prior number of votes for an account as of a block number
 * @dev Block number must be a finalized block or else this function will revert to prevent misinformation.
 * @param account The address of the account to check
 * @param blockNumber The block number to get the vote balance at
 * @return The number of votes the account had as of the given block
 */
function getPriorVotes(address account, uint blockNumber)
    external
    view
    returns (uint256)
{
    require(blockNumber < block.number, "YAX::getPriorVotes: not yet determined");

    uint32 nCheckpoints = numCheckpoints[account];
    if (nCheckpoints == 0) {
        return 0;
    }

    // First check most recent balance
    if (checkpoints[account][nCheckpoints - 1].fromBlock <= blockNumber) {
        return checkpoints[account][nCheckpoints - 1].votes;
    }

    // Next check implicit zero balance
    if (checkpoints[account][0].fromBlock > blockNumber) {
        return 0;
    }

    uint32 lower = 0;
    uint32 upper = nCheckpoints - 1;
    while (upper > lower) {
        uint32 center = upper - (upper - lower) / 2; // ceil, avoiding overflow
        Checkpoint memory cp = checkpoints[account][center];
        if (cp.fromBlock == blockNumber) {
            return cp.votes;
        } else if (cp.fromBlock < blockNumber) {
            lower = center;
        } else {
            upper = center - 1;
        }
    }
    return checkpoints[account][lower].votes;
}

function _delegate(address delegator, address delegatee)
    internal
{
    address currentDelegate = _delegates[delegator];
    uint256 delegatorBalance = balanceOf(delegator); // balance of underlying YAXs (not scaled);
    _delegates[delegator] = delegatee;

    emit DelegateChanged(delegator, currentDelegate, delegatee);

    _moveDelegates(currentDelegate, delegatee, delegatorBalance);
}

function _moveDelegates(address srcRep, address dstRep, uint256 amount) internal {
    if (srcRep != dstRep && amount > 0) {
        if (srcRep != address(0)) {
            // decrease old representative
            uint32 srcRepNum = numCheckpoints[srcRep];
            uint256 srcRepOld = srcRepNum > 0 ? checkpoints[srcRep][srcRepNum - 1].votes : 0;
            uint256 srcRepNew = srcRepOld.sub(amount);
            _writeCheckpoint(srcRep, srcRepNum, srcRepOld, srcRepNew);
        }

        if (dstRep != address(0)) {
            // increase new representative
            uint32 dstRepNum = numCheckpoints[dstRep];
            uint256 dstRepOld = dstRepNum > 0 ? checkpoints[dstRep][dstRepNum - 1].votes : 0;
            uint256 dstRepNew = dstRepOld.add(amount);
            _writeCheckpoint(dstRep, dstRepNum, dstRepOld, dstRepNew);
        }
    }
}

function _writeCheckpoint(
    address delegatee,
    uint32 nCheckpoints,
    uint256 oldVotes,
    uint256 newVotes
)
    internal
{
    uint32 blockNumber = safe32(block.number, "YAX::_writeCheckpoint: block number exceeds 32 bits");

    if (nCheckpoints > 0 && checkpoints[delegatee][nCheckpoints - 1].fromBlock == blockNumber) {
        checkpoints[delegatee][nCheckpoints - 1].votes = newVotes;
    } else {
        checkpoints[delegatee][nCheckpoints] = Checkpoint(blockNumber, newVotes);
        numCheckpoints[delegatee] = nCheckpoints + 1;
    }

    emit DelegateVotesChanged(delegatee, oldVotes, newVotes);
}

function safe32(uint n, string memory errorMessage) internal pure returns (uint32) {
    require(n < 2 ** 32, errorMessage);
    return uint32(n);
}

function getChainId() internal pure returns (uint) {
    uint256 chainId;
    assembly {chainId := chainid()}
    return chainId;
Haz077 commented 2 years ago

I don't see where that can happen, not enough info.

uN2RVw5q commented 2 years ago

I agree, this report is not clear. I would recommend adding an "invalid" tag.

GalloDaSballo commented 2 years ago

I also don't understand the report Invalid