Skip to content

Instantly share code, notes, and snippets.

Last active March 5, 2024 21:41
Show Gist options
  • Save hm3736485/96ff835e4df4584437449da5fdc17759 to your computer and use it in GitHub Desktop.
Save hm3736485/96ff835e4df4584437449da5fdc17759 to your computer and use it in GitHub Desktop.
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.6.0 <0.9.0;
import {Test, console2} from "lib/forge-std/src/Test.sol";
/// Attacker TX via orbit bridge API:
contract BlogTest is Test {
address public constant ETH_VAULT_PROXY_CONTRACT = 0x1Bf68A9d1EaEe7826b3593C20a0ca93293cb489a;
address public constant ATTACKER = 0x9263e7873613DDc598a701709875634819176AfF;
// Orbit hub contract on orbit mainnet :
address public constant HUB_CONTRACT = 0xB5680a55d627c52DE992e3EA52a86f19DA475399;
// "Orbit Bridge Exploiter 11", one of many EOA accounts where stolen funds were sent.
address public constant ETH_WALLET_ATTACKER = 0x27E2cc59a64D705a6c3D3d306186c2a55DcD5710;
/// seems to be another wallet used by the attacker where USDT was sent.
address public constant TETHER_WALLET_ATTACKER = 0xBD2830BA5357ec1EB49874E9Df14317229BA06D2;
address public constant TETHER_TOKEN_CONTRACT = 0xdAC17F958D2ee523a2206206994597C13D831ec7;
/* validator addresses, taken from:
address public constant ORBIT_BRIDGE_HASHQUARK_GOVERNANCE_VALIDATOR = 0x6013f0B3ffE1fFdcA3Fc6A8cd705b1Af048F7437;
address public constant ORBIT_BRIDGE_DSRV_GOVERNANCE_VALIDATOR = 0x3924Ac70075078A7713f543b72e3F8817ecEc646;
address public constant ORBIT_BRIDGE_KLAYTN_GOVERNANCE_VALIDATOR = 0x34EBf4f74a881eB63F666E63ce1Ff2F287CA5a8b;
address public constant ORBIT_BRIDGE_EVERSTAKE_GOVERNANCE_VALIDATOR = 0xa6dc28CbcB2f8060a00b4FA67F9b67775AC5a3a1;
address public constant ORBIT_BRIDGE_AVALANCHE_GOVERNANCE_VALIDATOR = 0x7F4b332611818aE13c76f9222e2229f274Ded9BD;
address public constant ORBIT_BRIDGE_MOONRIVER_GOVERNANCE_VALIDATOR = 0x595f1a527527Fa28A8C5f294c49E51B9799fdAF0;
address public constant ORBIT_BRIDGE_OZYS_GOVERNANCE_VALIDATOR = 0x8a3F117Ef3b40f1661Dedf7f28fC33E7b6fae4F8;
address public constant ORBIT_BRIDGE_ICON_GOVERNANCE_VALIDATOR = 0x1c0Cd56F1c3E2cF13B9B44dBE5529104bade543E;
address public constant ORBIT_BRIDGE_ETH_GOVERNANCE_VALIDATOR = 0x3b6590Ff12Ba188e465395E1610D8368613054B0;
address public constant ORBIT_BRIDGE_DXM_GOVERNANCE_VALIDATOR = 0x67C3c784C49d9ab8757ADb71491df1A1B38FbFA8;
// bytes32s[0] arg re-used for all withdraw txs
// bytes32s [0]:govId, [1]:txHash
// txHash is transaction hash on orbit chain
bytes32 public constant GOV_ID = 0x50f408f4b0fb17bf4f5143de4bd95802410d00422f008e9deef06fb101a0f060;
string public constant chain = "ETH";
IEthVault iEthVault;
IERC20 usdc;
IERC20 weth;
IERC20 wbtc;
IERC20 dai;
IERC20 tether;
IERC20 orcToken;
string infuraUrl = vm.envString("MAINNET");
uint256 blockHeight = 18908122; //block height before 30M USDT theft on eth mainnet - 1
uint256 mainnetFork = vm.createFork(infuraUrl);
function setUp() public {
// fork and set block height
console2.log("Forking from ETH mainnet at height: %s" , blockHeight);
vm.label(ATTACKER, "Attacker");
vm.label(ETH_VAULT_PROXY_CONTRACT, "ETH Vault Proxy");
//30 Mil USDT stolen on ETH mainnet:
function test_USDT_validate() public {
string memory fromChain = "ORBIT";
address fromAddr = ATTACKER;
address toAddr = 0xDAdfa3Ccd40Fc3d5A0164c6f9444f60163cCBf3b;
address token = TETHER_TOKEN_CONTRACT;
bytes32[] memory bytes32s = new bytes32[](2);
bytes32s[0] = GOV_ID;
bytes32s[1] = 0x8c799442963997a34dc0749f9d02433d3d79839fea19dfd6a3ca190b8a231a2b;
uint256[] memory uints = new uint256[](3);
uints[0] = 30000000000000; // 30 million USDT
uints[1] = 6;
uints[2] = 8729;
uint8[] memory v = new uint8[](15);
v[0] = 27;
v[1] = 27;
v[2] = 27;
v[3] = 28;
v[4] = 28;
v[5] = 28;
v[6] = 28;
v[7] = 27;
v[8] = 27;
v[9] = 27;
v[10] = 27;
v[11] = 27;
v[12] = 28;
v[13] = 27;
v[14] = 27;
bytes32[] memory r = new bytes32[](15);
r[0] = 0x7baf2dcc87b95c2cc498df55386e59f88b442a755346b0ce85523afdc189c014;
r[1] = 0xd6c2cb91e3c8b2a3c1c786a4597cd7a21f4be946c718c7bb67cb817ce856e1bd;
r[2] =0x60accec86324e6930525578b783ec0baaa4653db2493627e892933e767ed57dc;
r[3] =0xfbc3f634967093418e28f19def2ea7f1a924b37e8eacde859876253e7147f9f8;
r[4] =0x8d716a957cdb9546fc9bb45f38b6b75d11e130d9293782f8530053d05f56701b;
r[5] =0x3ed045d5b28e3d89b49da98b072580f4e4d91c0dc8d385996107e7c797bf1f3e;
r[6] =0x12797b354f0f691dfae7f9ebc971b58a8d9af5bfd632cacfae5ee3cdb6b3495e;
r[7] =0x596742ec19413368855e579ee22395a1ce0e79163321d0131eb65882b17563e9;
r[8] =0x5a65f067846e6d11d96e5861347349072c8083750e88ea5f7cbcb90a70d465ac;
r[9] =0xbd3d7b5909b382517fc61f927eefe06dda7ebde9ab8213defe2cbe241ce0cc37;
r[10] =0x66f3c23392ce395f2acb35f506333cce8c78d8f36a68eece8dfd5d49af0e8c2b;
r[11] =0xd040f048c55b0ce2999b2767ae44aa693069229cbd0dc9082d539852c08d1b6f;
r[12] =0x899db261fef30e64302c0e8858153d0a5db060637ad6aaee4d7c7adce19a332a;
r[13] =0xb104fa1e7978982a1294adc586f2f95dc21f13d0207bfe73922e5655d19dc71e;
r[14] =0x0ba2460bfbf4dab27f764ce9dfe2c68bdfcfd895cac1460c3c9b4103d30a7475;
bytes32[] memory s = new bytes32[](15);
s[0] =0x75caf0ae5e4722446c1829f6cf41f4958f4cef631fee8c5efc30613bbe44d5c1;
s[1] =0x2c7ee8df20074f16c51e93fa2ace8d809da2b0ca1e2e97471d4802cd5a370220;
s[2] =0x36097971dc91fd4ae0eafbad9b0f9cc6c5d7fe9334c04e6bd7538b75378d9d04;
s[3] =0x2216ca0d9413f1d05936bf6f47e39c63d54021644221bca66b126192f6d59297;
s[4] =0x4892247544ff45cd19cf8add312a296a48402d43909f5f01b7070404b4e30937;
s[5] =0x0d9264fbd050bc5654c19c695c802185799f570383b8e3c9fc1909fcd5c83eb9;
s[6] =0x118b1bad3a050cdc6c53a42c5161bf9ff719cfcf8c9d0335cd2fac58931abed9;
s[7] =0x69af95754cc75adb06f70b69fc092261d7931c0567606888851294753b7f1e70;
s[8] =0x28ec8db94c4ce5c82f21470a515111de74276e633b30c02d670359e9b9c4eb13;
s[9] =0x7ab97c30070d35d41ea41d7103e8a6f4c28478b5af1fa655640d81e96fbf70f9;
s[10] =0x4fd23d8aeb626281096d48caf5b32a3dee2b323047acecaa42fb7fb1b004c1db;
s[11] =0x66a628a5aab5049d6585009da7545188004d11d2847c1c57e2d0c384612b56f0;
s[12] =0x6167dfff0a2bd420d981f50ec8ebe0588c6c29a66ee18f4c81f154725f432385;
s[13] =0x53c918a35c619569ccd6ff484039ec9cd456ca2e653b4d922ec83acb1c430f6e;
s[14] =0x44b891c61d62913a9aaa1e19725e7a87b510b112cba8aeee48cd1e85ac867eb8;
bytes32 whash = sha256(abi.encodePacked(HUB_CONTRACT, fromChain, chain, fromAddr, toAddr, token, bytes32s, uints));
uint validatorCount = 0;
address[] memory vaList = new address[](iEthVault.getOwners().length);
address[] memory knownValidators = new address[](10);
uint i=0;
uint j=0;
for(i; i<v.length; i++){
address va = ecrecover(whash,v[i],r[i],s[i]);
if(iEthVault.isOwner(va)){ // confirming if the supplied signatures originate from EthVault's validator in isOwner mapping.
for(j=0; j<validatorCount; j++){
require(vaList[j] != va);
require(knownValidators[j] != va);
vaList[validatorCount] = va;
knownValidators[validatorCount] = va;
validatorCount += 1;
assertGe(validatorCount, iEthVault.required()); // confirm that at least 7 validators signed the whash
assertEq(validatorCount, knownValidators.length); // confirm that 10 validators signed the transaction
mig.validate call function args:
Method: a322da5a
[000]: 000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266
[020]: 6552e7dc22a3356c4593095059941a5247270b74e3c1201b769c5d54624dafc1
[040]: 000000000000000000000000000000000000000000000000000000000000001c
[060]: d7ff16e9b520d0744f40c3acd960c55700f0677738e5bd1041b9d5d8190b8871
[080]: 5ef979ccf9d5347bec4a6cee36324dfa81557649505e4871c704a4b6d645e74b
function test_api_validate() public {
console2.log("validating sigs derived from validator API");
// account used to sign sigHash in testing validator API
address apiSigner = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266;
// supplied hash
bytes32 apiSigHash = 0x6552e7dc22a3356c4593095059941a5247270b74e3c1201b769c5d54624dafc1;
address fromAddr = ATTACKER;
address toAddr = ETH_WALLET_ATTACKER;
address token = address(0);
string memory fromChain = "ORBIT";
bytes32[] memory bytes32s = new bytes32[](2);
bytes32s[0] = GOV_ID;
bytes32s[1] = 0xce133a5bf0a53be73935fa24abe31c509a912e3220c17afc67150f8b47a886a6;
uint256[] memory uints = new uint256[](3);
uints[0] = 200199600000000000;
uints[1] = 18;
uints[2] = 8646;
uint8[] memory v = new uint8[](1);
v[0] = 28;
bytes32[] memory r = new bytes32[](1);
r[0] = 0xd7ff16e9b520d0744f40c3acd960c55700f0677738e5bd1041b9d5d8190b8871;
bytes32[] memory s = new bytes32[](1);
s[0] = 0x5ef979ccf9d5347bec4a6cee36324dfa81557649505e4871c704a4b6d645e74b;
bytes32 whash = sha256(abi.encodePacked(HUB_CONTRACT, fromChain, chain, fromAddr, toAddr, token, bytes32s, uints));
// confirm hash generated on chain matches hash supplied to API
assertEq(whash, apiSigHash);
address va = ecrecover(whash,v[0],r[0],s[0]);
// confirm api signer account signed hash generated on chain
assertEq(va, apiSigner);
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.6.0 <0.9.0;
// OpenZeppelin Contracts (last updated v4.5.0) (token/ERC20/IERC20.sol)
pragma solidity ^0.8.0;
* @dev Interface of the ERC20 standard as defined in the EIP.
interface IERC20 {
* @dev Returns the amount of tokens in existence.
function totalSupply() external view returns (uint256);
* @dev Returns the amount of tokens owned by `account`.
function balanceOf(address account) external view returns (uint256);
* @dev Moves `amount` tokens from the caller's account to `to`.
* Returns a boolean value indicating whether the operation succeeded.
* Emits a {Transfer} event.
function transfer(address to, uint256 amount) external returns (bool);
* @dev Returns the remaining number of tokens that `spender` will be
* allowed to spend on behalf of `owner` through {transferFrom}. This is
* zero by default.
* This value changes when {approve} or {transferFrom} are called.
function allowance(address owner, address spender)
returns (uint256);
* @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
* Returns a boolean value indicating whether the operation succeeded.
* IMPORTANT: Beware that changing an allowance with this method brings the risk
* that someone may use both the old and the new allowance by unfortunate
* transaction ordering. One possible solution to mitigate this race
* condition is to first reduce the spender's allowance to 0 and set the
* desired value afterwards:
* Emits an {Approval} event.
function approve(address spender, uint256 amount) external returns (bool);
* @dev Moves `amount` tokens from `from` to `to` using the
* allowance mechanism. `amount` is then deducted from the caller's
* allowance.
* Returns a boolean value indicating whether the operation succeeded.
* Emits a {Transfer} event.
function transferFrom(
address from,
address to,
uint256 amount
) external returns (bool);
* @dev Emitted when `value` tokens are moved from one account (`from`) to
* another (`to`).
* Note that `value` may be zero.
event Transfer(address indexed from, address indexed to, uint256 value);
* @dev Emitted when the allowance of a `spender` for an `owner` is set by
* a call to {approve}. `value` is the new allowance.
event Approval(
address indexed owner,
address indexed spender,
uint256 value
interface IEthVault {
struct Transaction {
address destination;
uint value;
bytes data;
bool executed;
function getVersion() external pure returns(string memory);
function isOwner(address addr) external returns(bool);
function farms(address addr) external returns(address payable);
function policyAdmin() external returns(address);
function transactions(uint _i) external returns (address dest, uint value, bytes memory data, bool executed);
function getOwners() external view returns(address[] memory);
function isUsedWithdrawal(bytes32 b) external returns(bool);
function confirmTransaction(uint transactionId) external;
function submitTransaction(address destination, uint value, bytes memory data) external returns (uint transactionId);
function getConfirmations(uint transactionId) external view returns (address[] memory _confirmations);
function changeActivate(bool activate) external;
function setTetherAddress(address tether) external;
function getChainId(string memory _chain) external view returns(bytes32);
function setValidChain(string memory _chain, bool valid) external;
function deposit(string memory toChain, bytes memory toAddr) payable external;
function depositToken(address token, string memory toChain, bytes memory toAddr, uint amount) external;
function addOwner(address owner) external;
function executeTransaction(uint transactionId) external;
function addTransaction(address destination, uint value, bytes memory data) external returns (uint transactionid);
function isConfirmed(uint transactionId)
returns (bool);
function replaceOwner(address owner, address newOwner)
function required() external returns (uint);
function getTransactionCount(bool pending, bool executed)
returns (uint count);
function getTransactionIds(uint from, uint to, bool pending, bool executed)
returns (uint[] memory _transactionIds);
function governance_() external view returns (address);
// Fix Data Info
///@param bytes32s [0]:govId, [1]:txHash
///@param uints [0]:amount, [1]:decimals
function withdraw(
address hubContract,
string memory fromChain,
bytes memory fromAddr,
bytes memory toAddr,
bytes memory token,
bytes32[] memory bytes32s,
uint[] memory uints,
uint8[] memory v,
bytes32[] memory r,
bytes32[] memory s
) external;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment