SGT: Early Timelock Withdraw Post-Mortem

Dapp Whisperer
BadgerDAO
Published in
4 min readJun 24, 2021

Context

Around 10:30 am CET on Jun 23, 2021 a tweet reported that a developer of SharedStake (acting alone) exploited a feature in a timelock contract under their ownership to pull vested SGT tokens out before the intended release date. The tokens were subsequently dumped onto the market.

Because the timelock contract used was functionally identical to the SmartTimeLock contract in the Badger repo, we wanted to clarify the issue.

The actual draining of funds from the SGT timelock occurred at 7:10 CET. https://twitter.com/wassiecapital/status/1407600606018084868?s=21.

Discord message with an overview of the exploit

While the SmartTimelock contract is never used within the Badger systems, Badger community member WeebMcgee pointed out that the SmartVesting contract used to release the seeder allocation (10% of token supply) to the seed team over 1 year is subject to the same issue.

The team was aware of this vulnerability, and it is effectively mitigated by the fact that only the Governance Multisig (3/5) can trigger actions on the vesting contract. The contracts used for external vesting arrangements (such as for investors) did not use the vulnerable code for that reason.

Many new developers look to our code as an example, so we want to make sure no one copies a timelock that isn’t safe. We will patch the code in our repository with an appropriate fix and leave a comment as to the nature of the exploit mitigated.

SmartTimelock Exploit: Technical Analysis

Below is a technical assessment of the SGT exploit.

References

The call() method on SmartTimelock allows the beneficiary to execute arbitrary actions on behalf of the contract, as long as the locked token balance is not reduced as a result of the operation(s) executed.

function call(
address to,
uint256 value,
bytes calldata data
) external payable onlyBeneficiary() nonReentrant() returns (bool success) {
uint256 preAmount = token().balanceOf(address(this));
success = execute(to, value, data, gasleft());if (!_transferAllowed[to]) {
uint256 postAmount = token().balanceOf(address(this));
require(postAmount >= preAmount, "smart-vesting/locked-balance-check");
}
emit Call(to, value, data, _transferAllowed[to]);
}

While the token balance cannot be reduced during this call, this function can be used to grant approval to another contract to pull the funds in a later transaction, bypassing this safeguard.

This means that a malicious owner can pull the funds at any time (circumventing the purpose of the contract, which is to force the funds to be vested)

Exploit Flow

The beneficiary of the timelock used call() to grant an address under their control approval to use the funds by calling approve() on the vested ERC20 token contract.

ERC20.approve(MALICIOUS_ADDRESS, type(uint256).max)

That address can now use the standard ERC20 method transferFrom() on the vested token to pull the funds at their discretion.

// This will pull all funds from the TimeLock
ERC20.safeTransferFrom(TimeLockAddress, ERC20.balanceOf(TimeLockAddress))

Malicious Address

https://etherscan.io/address/0xa1feaf41d843d53d0f6bed86a8cf592ce21c409e

Target Contract

https://etherscan.io/address/0xa23179be88887804f319c047e88fdd4dd4867ef5#code

TX 1: Malicious approval (will allow to drain via transferFrom)

https://etherscan.io/tx/0x466bca01f2648ea855a018edce701d8bb9ca1b53853b076dfe15da7ea379cf39

TX 2: Malicious transferFrom (Drain all funds)

https://etherscan.io/tx/0xe5c00ca06fdde2b89db928a7f1266f1a5b17cfb123ee7fcd9fa640a102763d06

Mitigations

5 days ago a white hat submitted a PR to SharedStake to fix the vulnerability.

Other Takeaways

A key, if somewhat tangential, lesson from the SGT incident is to not give control over sensitive parts of your system to a single developer. All sensitive functions should be gated to a more robust governance structure during or immediately after deploy.

In retrospect, we at Badger should have also patched our public codebase to alert potential users of the code to the issue.

Whitehat Response

Badger Systems offers a bug-bounty of up to $750,000 USD for finding vulnerabilities in our live code. We retain Immunifi to manage this bounty for us. As part of this, they advertise our bounty to a community of crowdsourced white-hats and triage bugs and make decisions on payouts. At 11:54 CET, we received a note from Immunifi about our seed fund smart timelock. While we were already aware of this issue, and had classified it as mitigated, we are delighted that our bug bounty is working as intended. To our knowledge, the first reporter was WeebMcgee, who first logged into our discord at 9:21 CET looking for a team member to discuss this problem with.

While we do not see this security incident as an imminent threat to Badger operations or holdings, it has always been our intention to engage the whitehat community in every way possible.

--

--