From a626744ead77b528fae437e3dd03b15fa35c3282 Mon Sep 17 00:00:00 2001 From: real Date: Sun, 3 Nov 2024 20:59:20 +0300 Subject: [PATCH 1/6] add payload --- src/MayanCircle.sol | 208 ++++++++++++++++++++++++++------------------ 1 file changed, 124 insertions(+), 84 deletions(-) diff --git a/src/MayanCircle.sol b/src/MayanCircle.sol index 5b819a7..2d71281 100644 --- a/src/MayanCircle.sol +++ b/src/MayanCircle.sol @@ -23,7 +23,8 @@ contract MayanCircle is ReentrancyGuard { uint32 public immutable localDomain; uint16 public immutable auctionChainId; bytes32 public immutable auctionAddr; - bytes32 public immutable solanaEmitter; + bytes32 public solanaEmitter; // TODO: immutable + bytes32 public suiEmitter; // TODO: immutable uint8 public consistencyLevel; address public guardian; address nextGuardian; @@ -34,6 +35,7 @@ contract MayanCircle is ReentrancyGuard { uint8 constant ETH_DECIMALS = 18; uint32 constant SOLANA_DOMAIN = 5; uint16 constant SOLANA_CHAIN_ID = 1; + uint16 constant SUI_CHAIN_ID = 21; event OrderFulfilled(uint32 sourceDomain, uint64 sourceNonce, uint256 amount); event OrderRefunded(uint32 sourceDomain, uint64 sourceNonce, uint256 amount); @@ -50,6 +52,7 @@ contract MayanCircle is ReentrancyGuard { error InvalidDestAddr(); error InvalidMintRecipient(); error InvalidRedeemFee(); + error InvalidPayload(); enum Action { NONE, @@ -61,6 +64,8 @@ contract MayanCircle is ReentrancyGuard { } struct Order { + uint8 action; + uint8 payloadType; bytes32 trader; uint16 sourceChain; bytes32 tokenIn; @@ -75,6 +80,8 @@ contract MayanCircle is ReentrancyGuard { bytes32 referrerAddr; uint8 referrerBps; uint8 protocolBps; + uint64 cctpSourceNonce; + uint32 cctpSourceDomain; } struct OrderParams { @@ -118,12 +125,25 @@ contract MayanCircle is ReentrancyGuard { struct BridgeWithFeeMsg { uint8 action; - uint8 payloadId; + uint8 payloadType; uint64 cctpNonce; uint32 cctpDomain; bytes32 destAddr; uint64 gasDrop; uint64 redeemFee; + uint64 burnAmount; + bytes32 burnToken; + bytes32 customPayload; + } + + struct RedeemParams { + uint8 payloadType; + bytes32 destAddr; + uint64 gasDrop; + uint64 redeemFee; + uint64 burnAmount; + bytes32 burnToken; + bytes32 customPayload; } struct UnlockFeeMsg { @@ -170,6 +190,7 @@ contract MayanCircle is ReentrancyGuard { uint16 _auctionChainId, bytes32 _auctionAddr, bytes32 _solanaEmitter, + bytes32 _suiEmitter, uint8 _consistencyLevel ) { cctpTokenMessenger = ITokenMessenger(_cctpTokenMessenger); @@ -178,6 +199,7 @@ contract MayanCircle is ReentrancyGuard { auctionChainId = _auctionChainId; auctionAddr = _auctionAddr; solanaEmitter = _solanaEmitter; + suiEmitter = _suiEmitter; consistencyLevel = _consistencyLevel; localDomain = ITokenMessenger(_cctpTokenMessenger).localMessageTransmitter().localDomain(); guardian = msg.sender; @@ -189,7 +211,9 @@ contract MayanCircle is ReentrancyGuard { uint64 redeemFee, uint64 gasDrop, bytes32 destAddr, - CctpRecipient memory recipient + CctpRecipient memory recipient, + uint8 payloadType, + bytes32 customPayload ) external payable nonReentrant returns (uint64 sequence) { if (paused) { revert Paused(); @@ -207,19 +231,22 @@ contract MayanCircle is ReentrancyGuard { BridgeWithFeeMsg memory bridgeMsg = BridgeWithFeeMsg({ action: uint8(Action.BRIDGE_WITH_FEE), - payloadId: 1, + payloadType: payloadType, cctpNonce: ccptNonce, cctpDomain: localDomain, destAddr: destAddr, gasDrop: gasDrop, - redeemFee: redeemFee + redeemFee: redeemFee, + burnAmount: uint64(burnAmount), + burnToken: bytes32(uint256(uint160(tokenIn))), + customPayload: customPayload }); - bytes memory encoded = encodeBridgeWithFee(bridgeMsg); + bytes memory payload = abi.encodePacked(keccak256(encodeBridgeWithFee(bridgeMsg))); sequence = wormhole.publishMessage{ value : msg.value - }(0, encoded, consistencyLevel); + }(0, payload, consistencyLevel); } function bridgeWithLockedFee( @@ -275,6 +302,8 @@ contract MayanCircle is ReentrancyGuard { require(protocolBps <= 50, 'invalid protocol bps'); Order memory order = Order({ + action: uint8(Action.SWAP), + payloadType: 1, trader: bytes32(uint256(uint160(msg.sender))), sourceChain: wormhole.chainId(), tokenIn: bytes32(uint256(uint160(params.tokenIn))), @@ -288,60 +317,62 @@ contract MayanCircle is ReentrancyGuard { deadline: params.deadline, referrerAddr: params.referrerAddr, referrerBps: params.referrerBps, - protocolBps: protocolBps + protocolBps: protocolBps, + cctpSourceNonce: ccptNonce, + cctpSourceDomain: cctpTokenMessenger.localMessageTransmitter().localDomain() }); bytes memory encodedOrder = encodeOrder(order); - encodedOrder = encodedOrder.concat(abi.encodePacked(ccptNonce, cctpTokenMessenger.localMessageTransmitter().localDomain())); - - bytes32 orderHash = keccak256(encodedOrder); - OrderMsg memory orderMsg = OrderMsg({ - action: uint8(Action.SWAP), - payloadId: 1, - orderHash: orderHash - }); - - bytes memory encodedMsg = encodeOrderMsg(orderMsg); + bytes memory payload = abi.encodePacked(keccak256(encodedOrder)); wormhole.publishMessage{ value : msg.value - }(0, encodedMsg, consistencyLevel); + }(0, payload, consistencyLevel); } - function redeemWithFee(bytes memory cctpMsg, bytes memory cctpSigs, bytes memory encodedVm) external nonReentrant payable { + function redeemWithFee(bytes memory cctpMsg, bytes memory cctpSigs, bytes memory encodedVm, RedeemParams memory redeemParams) external nonReentrant payable { (IWormhole.VM memory vm, bool valid, string memory reason) = wormhole.parseAndVerifyVM(encodedVm); require(valid, reason); - if (vm.emitterAddress != solanaEmitter && truncateAddress(vm.emitterAddress) != address(this)) { - revert InvalidEmitter(); - } - - BridgeWithFeeMsg memory redeemMsg = parseBridgeWithFee(vm.payload); - if (redeemMsg.action != uint8(Action.BRIDGE_WITH_FEE)) { - revert InvalidAction(); - } - - uint256 denormalizedGasDrop = deNormalizeAmount(redeemMsg.gasDrop, ETH_DECIMALS); - if (msg.value != denormalizedGasDrop) { - revert InvalidGasDrop(); + if (vm.emitterAddress != solanaEmitter + && vm.emitterAddress != suiEmitter + && truncateAddress(vm.emitterAddress) != address(this)) { + revert InvalidEmitter(); } - uint32 cctpSourceDomain = cctpMsg.toUint32(4); - uint64 cctpNonce = cctpMsg.toUint64(12); - bytes32 cctpSourceToken = cctpMsg.toBytes32(120); - if (truncateAddress(cctpMsg.toBytes32(152)) != address(this)) { revert InvalidMintRecipient(); } - if (cctpSourceDomain != redeemMsg.cctpDomain) { - revert InvalidDomain(); + BridgeWithFeeMsg memory redeemMsg = BridgeWithFeeMsg({ + action: uint8(Action.BRIDGE_WITH_FEE), + payloadType: redeemParams.payloadType, + cctpNonce: cctpMsg.toUint64(12), + cctpDomain: cctpMsg.toUint32(4), + destAddr: redeemParams.destAddr, + gasDrop: redeemParams.gasDrop, + redeemFee: redeemParams.redeemFee, + burnAmount: uint64(cctpMsg.toUint256(184)), + burnToken: cctpMsg.toBytes32(120), + customPayload: redeemParams.customPayload + }); + + if (vm.payload.length != 32) { + revert InvalidPayload(); } - if (cctpNonce != redeemMsg.cctpNonce) { - revert InvalidNonce(); + if (keccak256(encodeBridgeWithFee(redeemMsg)) != vm.payload.toBytes32(0)) { + revert InvalidPayload(); + } + if (redeemMsg.payloadType == 2 && msg.sender != truncateAddress(redeemMsg.destAddr)) { + revert Unauthorized(); + } + + uint256 denormalizedGasDrop = deNormalizeAmount(redeemMsg.gasDrop, ETH_DECIMALS); + if (msg.value != denormalizedGasDrop) { + revert InvalidGasDrop(); } - address localToken = cctpTokenMessenger.localMinter().getLocalToken(cctpSourceDomain, cctpSourceToken); + address localToken = cctpTokenMessenger.localMinter().getLocalToken(redeemMsg.cctpDomain, redeemMsg.burnToken); uint256 amount = IERC20(localToken).balanceOf(address(this)); bool success = cctpTokenMessenger.localMessageTransmitter().receiveMessage(cctpMsg, cctpSigs); if (!success) { @@ -416,10 +447,10 @@ contract MayanCircle is ReentrancyGuard { (IWormhole.VM memory vm, bool valid, string memory reason) = wormhole.parseAndVerifyVM(encodedVm); require(valid, reason); - if (vm.emitterChainId == SOLANA_CHAIN_ID) { - require(vm.emitterAddress == solanaEmitter, 'invalid solana emitter'); - } else { - require(truncateAddress(vm.emitterAddress) == address(this), 'invalid evm emitter'); + if (vm.emitterAddress != solanaEmitter + && vm.emitterAddress != suiEmitter + && truncateAddress(vm.emitterAddress) != address(this)) { + revert InvalidEmitter(); } UnlockFeeMsg memory unlockMsg = parseUnlockFeeMsg(vm.payload); @@ -444,7 +475,9 @@ contract MayanCircle is ReentrancyGuard { (IWormhole.VM memory vm1, bool valid1, string memory reason1) = wormhole.parseAndVerifyVM(encodedVm1); require(valid1, reason1); - if (vm1.emitterAddress != solanaEmitter && truncateAddress(vm1.emitterAddress) != address(this)) { + if (vm1.emitterAddress != solanaEmitter + && vm1.emitterAddress != suiEmitter + && truncateAddress(vm1.emitterAddress) != address(this)) { revert InvalidEmitter(); } @@ -463,7 +496,9 @@ contract MayanCircle is ReentrancyGuard { (IWormhole.VM memory vm2, bool valid2, string memory reason2) = wormhole.parseAndVerifyVM(encodedVm2); require(valid2, reason2); - if (vm2.emitterAddress != solanaEmitter && truncateAddress(vm2.emitterAddress) != address(this)) { + if (vm2.emitterAddress != solanaEmitter + && vm2.emitterAddress != suiEmitter + && truncateAddress(vm2.emitterAddress) != address(this)) { revert InvalidEmitter(); } @@ -493,6 +528,8 @@ contract MayanCircle is ReentrancyGuard { bytes memory cctpMsg, bytes memory cctpSigs, bytes memory encodedVm, + OrderParams memory orderParams, + ExtraParams memory extraParams, address swapProtocol, bytes memory swapData ) public nonReentrant payable { @@ -506,14 +543,15 @@ contract MayanCircle is ReentrancyGuard { require(fulfillMsg.deadline >= block.timestamp, 'deadline passed'); require(msg.sender == truncateAddress(fulfillMsg.driver), 'invalid driver'); - uint32 cctpSourceDomain = cctpMsg.toUint32(4); - uint64 cctpSourceNonce = cctpMsg.toUint64(12); - bytes32 cctpSourceToken = cctpMsg.toBytes32(120); + Order memory order = recreateOrder(cctpMsg, orderParams, extraParams); + + bytes32 calculatedPayload = keccak256(encodeOrder(order)); - require(cctpSourceDomain == fulfillMsg.cctpDomain, 'invalid cctp domain'); - require(cctpSourceNonce == fulfillMsg.cctpNonce, 'invalid cctp nonce'); + if (vm.payload.length != 32 || calculatedPayload != vm.payload.toBytes32(0)) { + revert InvalidPayload(); + } - (address localToken, uint256 cctpAmount) = receiveCctp(cctpMsg, cctpSigs, cctpSourceDomain, cctpSourceToken); + (address localToken, uint256 cctpAmount) = receiveCctp(cctpMsg, cctpSigs); if (fulfillMsg.redeemFee > 0) { IERC20(localToken).safeTransfer(msg.sender, fulfillMsg.redeemFee); @@ -554,7 +592,7 @@ contract MayanCircle is ReentrancyGuard { amountOut ); - emit OrderFulfilled(cctpSourceDomain, cctpSourceNonce, amountOut); + emit OrderFulfilled(order.cctpSourceDomain, order.cctpSourceNonce, amountOut); } function refund( @@ -567,28 +605,19 @@ contract MayanCircle is ReentrancyGuard { (IWormhole.VM memory vm, bool valid, string memory reason) = wormhole.parseAndVerifyVM(encodedVm); require(valid, reason); - if (vm.emitterAddress != solanaEmitter && truncateAddress(vm.emitterAddress) != address(this)) { + if (vm.emitterAddress != solanaEmitter + && vm.emitterAddress != suiEmitter + && truncateAddress(vm.emitterAddress) != address(this)) { revert InvalidEmitter(); } - uint32 cctpSourceDomain = cctpMsg.toUint32(4); - uint64 cctpSourceNonce = cctpMsg.toUint64(12); - bytes32 cctpSourceToken = cctpMsg.toBytes32(120); - - (address localToken, uint256 amount) = receiveCctp(cctpMsg, cctpSigs, cctpSourceDomain, cctpSourceToken); + (address localToken, uint256 amount) = receiveCctp(cctpMsg, cctpSigs); - Order memory order = recreateOrder(cctpSourceToken, uint64(amount), orderParams, extraParams); - - bytes memory encodedOrder = encodeOrder(order); - encodedOrder = encodedOrder.concat(abi.encodePacked(cctpSourceNonce, cctpSourceDomain)); - bytes32 calculatedHash = keccak256(encodedOrder); + Order memory order = recreateOrder(cctpMsg, orderParams, extraParams); + bytes32 calculatedPayload = keccak256(encodeOrder(order)); - OrderMsg memory orderMsg = parseOrderMsg(vm.payload); - if (orderMsg.action != uint8(Action.SWAP)) { - revert InvalidAction(); - } - if (calculatedHash != orderMsg.orderHash) { - revert InvalidOrder(); + if (vm.payload.length != 32 || calculatedPayload != vm.payload.toBytes32(0)) { + revert InvalidPayload(); } require(order.deadline < block.timestamp, 'deadline not passed'); @@ -606,11 +635,13 @@ contract MayanCircle is ReentrancyGuard { IERC20(localToken).safeTransfer(msg.sender, order.redeemFee); IERC20(localToken).safeTransfer(destAddr, amount - order.redeemFee); - emit OrderRefunded(cctpSourceDomain, cctpSourceNonce, amount); + emit OrderRefunded(order.cctpSourceDomain, order.cctpSourceNonce, amount); } - function receiveCctp(bytes memory cctpMsg, bytes memory cctpSigs, uint32 cctpSourceDomain, bytes32 cctpSourceToken) internal returns (address, uint256) { - address localToken = cctpTokenMessenger.localMinter().getLocalToken(cctpSourceDomain, cctpSourceToken); + function receiveCctp(bytes memory cctpMsg, bytes memory cctpSigs) internal returns (address, uint256) { + uint32 cctpDomain = cctpMsg.toUint32(4); + bytes32 cctpSourceToken = cctpMsg.toBytes32(120); + address localToken = cctpTokenMessenger.localMinter().getLocalToken(cctpDomain, cctpSourceToken); uint256 amount = IERC20(localToken).balanceOf(address(this)); bool success = cctpTokenMessenger.localMessageTransmitter().receiveMessage(cctpMsg, cctpSigs); @@ -665,16 +696,17 @@ contract MayanCircle is ReentrancyGuard { } function recreateOrder( - bytes32 cctpSourceToken, - uint64 amountIn, + bytes memory cctpMsg, OrderParams memory params, ExtraParams memory extraParams ) internal pure returns (Order memory) { return Order({ + action: uint8(Action.SWAP), + payloadType: 1, trader: extraParams.trader, sourceChain: extraParams.sourceChainId, - tokenIn: cctpSourceToken, - amountIn: amountIn, + tokenIn: cctpMsg.toBytes32(120), + amountIn: uint64(cctpMsg.toUint256(184)), destAddr: params.destAddr, destChain: params.destChain, tokenOut: params.tokenOut, @@ -684,31 +716,39 @@ contract MayanCircle is ReentrancyGuard { deadline: params.deadline, referrerAddr: params.referrerAddr, referrerBps: params.referrerBps, - protocolBps: extraParams.protocolBps + protocolBps: extraParams.protocolBps, + cctpSourceNonce: cctpMsg.toUint64(12), + cctpSourceDomain: cctpMsg.toUint32(4) }); } function encodeBridgeWithFee(BridgeWithFeeMsg memory bridgeMsg) internal pure returns (bytes memory) { return abi.encodePacked( bridgeMsg.action, - bridgeMsg.payloadId, + bridgeMsg.payloadType, bridgeMsg.cctpNonce, bridgeMsg.cctpDomain, bridgeMsg.destAddr, bridgeMsg.gasDrop, - bridgeMsg.redeemFee + bridgeMsg.redeemFee, + bridgeMsg.burnAmount, + bridgeMsg.burnToken, + bridgeMsg.customPayload ); } function parseBridgeWithFee(bytes memory payload) internal pure returns (BridgeWithFeeMsg memory) { return BridgeWithFeeMsg({ - action: payload.toUint8(0), - payloadId: payload.toUint8(1), + action: uint8(Action.BRIDGE_WITH_FEE), + payloadType: payload.toUint8(1), cctpNonce: payload.toUint64(2), cctpDomain: payload.toUint32(10), destAddr: payload.toBytes32(14), gasDrop: payload.toUint64(46), - redeemFee: payload.toUint64(54) + redeemFee: payload.toUint64(54), + burnAmount: payload.toUint64(62), + burnToken: payload.toBytes32(70), + customPayload: payload.toBytes32(102) }); } From 71cd8ef9ab238fde06f10c783ef4cc964c61e6d8 Mon Sep 17 00:00:00 2001 From: real Date: Tue, 5 Nov 2024 23:07:18 +0300 Subject: [PATCH 2/6] use hash for unlock fee --- src/MayanCircle.sol | 486 ++++++++++++++++++++++++-------------------- 1 file changed, 267 insertions(+), 219 deletions(-) diff --git a/src/MayanCircle.sol b/src/MayanCircle.sol index 2d71281..351296a 100644 --- a/src/MayanCircle.sol +++ b/src/MayanCircle.sol @@ -23,8 +23,8 @@ contract MayanCircle is ReentrancyGuard { uint32 public immutable localDomain; uint16 public immutable auctionChainId; bytes32 public immutable auctionAddr; - bytes32 public solanaEmitter; // TODO: immutable - bytes32 public suiEmitter; // TODO: immutable + bytes32 public immutable solanaEmitter; + bytes32 public immutable suiEmitter; uint8 public consistencyLevel; address public guardian; address nextGuardian; @@ -32,11 +32,20 @@ contract MayanCircle is ReentrancyGuard { mapping(uint64 => FeeLock) public feeStorage; + mapping(uint32 => bytes32) public domainToCaller; + mapping(bytes32 => bytes32) public keyToMintRecipient; // key is domain + local token address + uint8 constant ETH_DECIMALS = 18; uint32 constant SOLANA_DOMAIN = 5; uint16 constant SOLANA_CHAIN_ID = 1; + uint32 constant SUI_DOMAIN = 8; uint16 constant SUI_CHAIN_ID = 21; + uint256 constant CCTP_DOMAIN_INDEX = 4; + uint256 constant CCTP_NONCE_INDEX = 12; + uint256 constant CCTP_TOKEN_INDEX = 120; + uint256 constant CCTP_AMOUNT_INDEX = 184; + event OrderFulfilled(uint32 sourceDomain, uint64 sourceNonce, uint256 amount); event OrderRefunded(uint32 sourceDomain, uint64 sourceNonce, uint256 amount); @@ -53,6 +62,8 @@ contract MayanCircle is ReentrancyGuard { error InvalidMintRecipient(); error InvalidRedeemFee(); error InvalidPayload(); + error CallerNotSet(); + error MintRecepientNotSet(); enum Action { NONE, @@ -78,6 +89,9 @@ contract MayanCircle is ReentrancyGuard { uint64 redeemFee; uint64 deadline; bytes32 referrerAddr; + } + + struct OrderFields { uint8 referrerBps; uint8 protocolBps; uint64 cctpSourceNonce; @@ -117,12 +131,6 @@ contract MayanCircle is ReentrancyGuard { uint256 redeemFee; } - struct CctpRecipient { - uint32 destDomain; - bytes32 mintRecipient; - bytes32 callerAddr; - } - struct BridgeWithFeeMsg { uint8 action; uint8 payloadType; @@ -136,7 +144,7 @@ contract MayanCircle is ReentrancyGuard { bytes32 customPayload; } - struct RedeemParams { + struct BridgeWithFeeParams { uint8 payloadType; bytes32 destAddr; uint64 gasDrop; @@ -148,16 +156,21 @@ contract MayanCircle is ReentrancyGuard { struct UnlockFeeMsg { uint8 action; - uint8 payloadId; + uint8 payloadType; uint64 cctpNonce; uint32 cctpDomain; bytes32 unlockerAddr; uint64 gasDrop; } + struct UnlockParams { + bytes32 unlockerAddr; + uint64 gasDrop; + } + struct UnlockRefinedFeeMsg { uint8 action; - uint8 payloadId; + uint8 payloadType; uint64 cctpNonce; uint32 cctpDomain; bytes32 unlockerAddr; @@ -167,20 +180,34 @@ contract MayanCircle is ReentrancyGuard { struct FulfillMsg { uint8 action; - uint8 payloadId; - uint16 destChainId; + uint8 payloadType; bytes32 destAddr; - bytes32 driver; + uint16 destChainId; bytes32 tokenOut; uint64 promisedAmount; uint64 gasDrop; + uint64 redeemFee; + uint64 deadline; bytes32 referrerAddr; uint8 referrerBps; uint8 protocolBps; - uint64 deadline; + uint64 cctpSourceNonce; + uint32 cctpSourceDomain; + bytes32 driver; + } + + struct FulfillParams { + bytes32 destAddr; + uint16 destChainId; + bytes32 tokenOut; + uint64 promisedAmount; + uint64 gasDrop; uint64 redeemFee; - uint32 cctpDomain; - uint64 cctpNonce; + uint64 deadline; + bytes32 referrerAddr; + uint8 referrerBps; + uint8 protocolBps; + bytes32 driver; } constructor( @@ -211,7 +238,7 @@ contract MayanCircle is ReentrancyGuard { uint64 redeemFee, uint64 gasDrop, bytes32 destAddr, - CctpRecipient memory recipient, + uint32 destDomain, uint8 payloadType, bytes32 customPayload ) external payable nonReentrant returns (uint64 sequence) { @@ -222,12 +249,17 @@ contract MayanCircle is ReentrancyGuard { revert InvalidRedeemFee(); } - uint256 burnAmount = IERC20(tokenIn).balanceOf(address(this)); IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn); - burnAmount = IERC20(tokenIn).balanceOf(address(this)) - burnAmount; - maxApproveIfNeeded(tokenIn, address(cctpTokenMessenger), burnAmount); - uint64 ccptNonce = cctpTokenMessenger.depositForBurnWithCaller(burnAmount, recipient.destDomain, recipient.mintRecipient, tokenIn, recipient.callerAddr); + maxApproveIfNeeded(tokenIn, address(cctpTokenMessenger), amountIn); + + uint64 ccptNonce = cctpTokenMessenger.depositForBurnWithCaller( + amountIn, + destDomain, + getMintRecipient(destDomain, tokenIn), + tokenIn, + getCaller(destDomain) + ); BridgeWithFeeMsg memory bridgeMsg = BridgeWithFeeMsg({ action: uint8(Action.BRIDGE_WITH_FEE), @@ -237,7 +269,7 @@ contract MayanCircle is ReentrancyGuard { destAddr: destAddr, gasDrop: gasDrop, redeemFee: redeemFee, - burnAmount: uint64(burnAmount), + burnAmount: uint64(amountIn), burnToken: bytes32(uint256(uint160(tokenIn))), customPayload: customPayload }); @@ -254,25 +286,25 @@ contract MayanCircle is ReentrancyGuard { uint256 amountIn, uint64 gasDrop, uint256 redeemFee, - CctpRecipient memory recipient + uint32 destDomain ) external nonReentrant returns (uint64 cctpNonce) { if (paused) { revert Paused(); } - if (recipient.destDomain == SOLANA_DOMAIN) { + if (destDomain == SOLANA_DOMAIN || destDomain == SUI_DOMAIN) { revert InvalidDomain(); } require(redeemFee > 0, 'zero redeem fee'); - uint256 burnAmount = IERC20(tokenIn).balanceOf(address(this)); IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn); - burnAmount = IERC20(tokenIn).balanceOf(address(this)) - burnAmount; - maxApproveIfNeeded(tokenIn, address(cctpTokenMessenger), burnAmount - redeemFee); - cctpNonce = cctpTokenMessenger.depositForBurnWithCaller(burnAmount - redeemFee, recipient.destDomain, recipient.mintRecipient, tokenIn, recipient.callerAddr); + maxApproveIfNeeded(tokenIn, address(cctpTokenMessenger), amountIn - redeemFee); + + bytes32 mintRecipient = getMintRecipient(destDomain, tokenIn); + cctpNonce = cctpTokenMessenger.depositForBurnWithCaller(amountIn - redeemFee, destDomain, mintRecipient, tokenIn, getCaller(destDomain)); feeStorage[cctpNonce] = FeeLock({ - destAddr: recipient.mintRecipient, + destAddr: mintRecipient, gasDrop: gasDrop, token: tokenIn, redeemFee: redeemFee @@ -281,7 +313,7 @@ contract MayanCircle is ReentrancyGuard { function createOrder( OrderParams memory params, - CctpRecipient memory recipient + uint32 destDomain ) external payable nonReentrant { if (paused) { revert Paused(); @@ -295,7 +327,9 @@ contract MayanCircle is ReentrancyGuard { IERC20(params.tokenIn).safeTransferFrom(msg.sender, address(this), params.amountIn); maxApproveIfNeeded(params.tokenIn, address(cctpTokenMessenger), params.amountIn); - uint64 ccptNonce = cctpTokenMessenger.depositForBurnWithCaller(params.amountIn, recipient.destDomain, recipient.mintRecipient, params.tokenIn, recipient.callerAddr); + + bytes32 cctpRecipient = getMintRecipient(destDomain, params.tokenIn); + uint64 ccptNonce = cctpTokenMessenger.depositForBurnWithCaller(params.amountIn, destDomain, cctpRecipient, params.tokenIn, getCaller(destDomain)); require(params.referrerBps <= 50, 'invalid referrer bps'); uint8 protocolBps = feeManager.calcProtocolBps(uint64(params.amountIn), params.tokenIn, params.tokenOut, params.destChain, params.referrerBps); @@ -315,14 +349,19 @@ contract MayanCircle is ReentrancyGuard { gasDrop: params.gasDrop, redeemFee: params.redeemFee, deadline: params.deadline, - referrerAddr: params.referrerAddr, + referrerAddr: params.referrerAddr + }); + + bytes memory encodedOrder = encodeOrder(order); + + OrderFields memory orderFields = OrderFields({ referrerBps: params.referrerBps, protocolBps: protocolBps, cctpSourceNonce: ccptNonce, cctpSourceDomain: cctpTokenMessenger.localMessageTransmitter().localDomain() }); - - bytes memory encodedOrder = encodeOrder(order); + + encodedOrder = encodedOrder.concat(encodeOrderFields(orderFields)); bytes memory payload = abi.encodePacked(keccak256(encodedOrder)); wormhole.publishMessage{ @@ -330,49 +369,37 @@ contract MayanCircle is ReentrancyGuard { }(0, payload, consistencyLevel); } - function redeemWithFee(bytes memory cctpMsg, bytes memory cctpSigs, bytes memory encodedVm, RedeemParams memory redeemParams) external nonReentrant payable { + function redeemWithFee(bytes memory cctpMsg, bytes memory cctpSigs, bytes memory encodedVm, BridgeWithFeeParams memory bridgeParams) external nonReentrant payable { (IWormhole.VM memory vm, bool valid, string memory reason) = wormhole.parseAndVerifyVM(encodedVm); require(valid, reason); - if (vm.emitterAddress != solanaEmitter - && vm.emitterAddress != suiEmitter - && truncateAddress(vm.emitterAddress) != address(this)) { - revert InvalidEmitter(); - } + validateEmitter(vm.emitterAddress); if (truncateAddress(cctpMsg.toBytes32(152)) != address(this)) { revert InvalidMintRecipient(); } - BridgeWithFeeMsg memory redeemMsg = BridgeWithFeeMsg({ - action: uint8(Action.BRIDGE_WITH_FEE), - payloadType: redeemParams.payloadType, - cctpNonce: cctpMsg.toUint64(12), - cctpDomain: cctpMsg.toUint32(4), - destAddr: redeemParams.destAddr, - gasDrop: redeemParams.gasDrop, - redeemFee: redeemParams.redeemFee, - burnAmount: uint64(cctpMsg.toUint256(184)), - burnToken: cctpMsg.toBytes32(120), - customPayload: redeemParams.customPayload - }); + BridgeWithFeeMsg memory bridgeMsg = recreateBridgeWithFee(bridgeParams, cctpMsg); if (vm.payload.length != 32) { revert InvalidPayload(); } - if (keccak256(encodeBridgeWithFee(redeemMsg)) != vm.payload.toBytes32(0)) { + + bytes32 calculatedPayload = keccak256(encodeBridgeWithFee(bridgeMsg)); + if (vm.payload.length != 32 || calculatedPayload != vm.payload.toBytes32(0)) { revert InvalidPayload(); } - if (redeemMsg.payloadType == 2 && msg.sender != truncateAddress(redeemMsg.destAddr)) { + + if (bridgeMsg.payloadType == 2 && msg.sender != truncateAddress(bridgeMsg.destAddr)) { revert Unauthorized(); } - uint256 denormalizedGasDrop = deNormalizeAmount(redeemMsg.gasDrop, ETH_DECIMALS); + uint256 denormalizedGasDrop = deNormalizeAmount(bridgeMsg.gasDrop, ETH_DECIMALS); if (msg.value != denormalizedGasDrop) { revert InvalidGasDrop(); } - address localToken = cctpTokenMessenger.localMinter().getLocalToken(redeemMsg.cctpDomain, redeemMsg.burnToken); + address localToken = cctpTokenMessenger.localMinter().getLocalToken(bridgeMsg.cctpDomain, bridgeMsg.burnToken); uint256 amount = IERC20(localToken).balanceOf(address(this)); bool success = cctpTokenMessenger.localMessageTransmitter().receiveMessage(cctpMsg, cctpSigs); if (!success) { @@ -380,9 +407,9 @@ contract MayanCircle is ReentrancyGuard { } amount = IERC20(localToken).balanceOf(address(this)) - amount; - IERC20(localToken).safeTransfer(msg.sender, uint256(redeemMsg.redeemFee)); - address recipient = truncateAddress(redeemMsg.destAddr); - IERC20(localToken).safeTransfer(recipient, amount - uint256(redeemMsg.redeemFee)); + IERC20(localToken).safeTransfer(msg.sender, uint256(bridgeMsg.redeemFee)); + address recipient = truncateAddress(bridgeMsg.destAddr); + IERC20(localToken).safeTransfer(recipient, amount - uint256(bridgeMsg.redeemFee)); payEth(recipient, denormalizedGasDrop, false); } @@ -406,7 +433,7 @@ contract MayanCircle is ReentrancyGuard { UnlockFeeMsg memory unlockMsg = UnlockFeeMsg({ action: uint8(Action.UNLOCK_FEE), - payloadId: 1, + payloadType: 1, cctpDomain: cctpSourceDomain, cctpNonce: cctpNonce, unlockerAddr: unlockerAddr, @@ -414,10 +441,11 @@ contract MayanCircle is ReentrancyGuard { }); bytes memory encodedMsg = encodeUnlockFeeMsg(unlockMsg); + bytes memory payload = abi.encodePacked(keccak256(encodedMsg)); sequence = wormhole.publishMessage{ value : wormholeFee - }(0, encodedMsg, consistencyLevel); + }(0, payload, consistencyLevel); } function refineFee(uint32 cctpNonce, uint32 cctpDomain, bytes32 destAddr, bytes32 unlockerAddr) external nonReentrant payable returns (uint64 sequence) { @@ -428,7 +456,7 @@ contract MayanCircle is ReentrancyGuard { UnlockRefinedFeeMsg memory unlockMsg = UnlockRefinedFeeMsg({ action: uint8(Action.UNLOCK_FEE_REFINE), - payloadId: 1, + payloadType: 1, cctpDomain: cctpDomain, cctpNonce: cctpNonce, unlockerAddr: unlockerAddr, @@ -437,26 +465,28 @@ contract MayanCircle is ReentrancyGuard { }); bytes memory encodedMsg = encodeUnlockRefinedFeeMsg(unlockMsg); + bytes memory payload = abi.encodePacked(keccak256(encodedMsg)); sequence = wormhole.publishMessage{ value : wormholeFee - }(0, encodedMsg, consistencyLevel); + }(0, payload, consistencyLevel); } - function unlockFee(bytes memory encodedVm) public nonReentrant { + function unlockFee( + bytes memory encodedVm, + UnlockFeeMsg memory unlockMsg + ) public nonReentrant { (IWormhole.VM memory vm, bool valid, string memory reason) = wormhole.parseAndVerifyVM(encodedVm); require(valid, reason); - if (vm.emitterAddress != solanaEmitter - && vm.emitterAddress != suiEmitter - && truncateAddress(vm.emitterAddress) != address(this)) { - revert InvalidEmitter(); - } + validateEmitter(vm.emitterAddress); - UnlockFeeMsg memory unlockMsg = parseUnlockFeeMsg(vm.payload); - if (unlockMsg.action != uint8(Action.UNLOCK_FEE)) { - revert InvalidAction(); + unlockMsg.action = uint8(Action.UNLOCK_FEE); + bytes32 calculatedPayload = keccak256(encodeUnlockFeeMsg(unlockMsg)); + if (vm.payload.length != 32 || calculatedPayload != vm.payload.toBytes32(0)) { + revert InvalidPayload(); } + if (unlockMsg.cctpDomain != localDomain) { revert InvalidDomain(); } @@ -471,19 +501,21 @@ contract MayanCircle is ReentrancyGuard { delete feeStorage[unlockMsg.cctpNonce]; } - function unlockFeeRefined(bytes memory encodedVm1, bytes memory encodedVm2) public nonReentrant { + function unlockFeeRefined( + bytes memory encodedVm1, + bytes memory encodedVm2, + UnlockFeeMsg memory unlockMsg, + UnlockRefinedFeeMsg memory refinedMsg + ) public nonReentrant { (IWormhole.VM memory vm1, bool valid1, string memory reason1) = wormhole.parseAndVerifyVM(encodedVm1); require(valid1, reason1); - if (vm1.emitterAddress != solanaEmitter - && vm1.emitterAddress != suiEmitter - && truncateAddress(vm1.emitterAddress) != address(this)) { - revert InvalidEmitter(); - } + validateEmitter(vm1.emitterAddress); - UnlockFeeMsg memory unlockMsg = parseUnlockFeeMsg(vm1.payload); - if (unlockMsg.action != uint8(Action.UNLOCK_FEE)) { - revert InvalidAction(); + unlockMsg.action = uint8(Action.UNLOCK_FEE); + bytes32 calculatedPayload1 = keccak256(encodeUnlockFeeMsg(unlockMsg)); + if (vm1.payload.length != 32 || calculatedPayload1 != vm1.payload.toBytes32(0)) { + revert InvalidPayload(); } if (unlockMsg.cctpDomain != localDomain) { revert InvalidDomain(); @@ -496,15 +528,12 @@ contract MayanCircle is ReentrancyGuard { (IWormhole.VM memory vm2, bool valid2, string memory reason2) = wormhole.parseAndVerifyVM(encodedVm2); require(valid2, reason2); - if (vm2.emitterAddress != solanaEmitter - && vm2.emitterAddress != suiEmitter - && truncateAddress(vm2.emitterAddress) != address(this)) { - revert InvalidEmitter(); - } + validateEmitter(vm2.emitterAddress); - UnlockRefinedFeeMsg memory refinedMsg = parseUnlockRefinedFee(vm2.payload); - if (refinedMsg.action != uint8(Action.UNLOCK_FEE_REFINE)) { - revert InvalidAction(); + refinedMsg.action = uint8(Action.UNLOCK_FEE_REFINE); + bytes32 calculatedPayload2 = keccak256(encodeUnlockRefinedFeeMsg(refinedMsg)); + if (vm2.payload.length != 32 || calculatedPayload2 != vm2.payload.toBytes32(0)) { + revert InvalidPayload(); } if (refinedMsg.destAddr != feeLock.destAddr) { @@ -528,8 +557,7 @@ contract MayanCircle is ReentrancyGuard { bytes memory cctpMsg, bytes memory cctpSigs, bytes memory encodedVm, - OrderParams memory orderParams, - ExtraParams memory extraParams, + FulfillParams memory params, address swapProtocol, bytes memory swapData ) public nonReentrant payable { @@ -539,14 +567,11 @@ contract MayanCircle is ReentrancyGuard { require(vm.emitterChainId == SOLANA_CHAIN_ID, 'invalid emitter chain'); require(vm.emitterAddress == auctionAddr, 'invalid solana emitter'); - FulfillMsg memory fulfillMsg = parseFulfillMsg(vm.payload); + FulfillMsg memory fulfillMsg = recreateFulfillMsg(params, cctpMsg); require(fulfillMsg.deadline >= block.timestamp, 'deadline passed'); require(msg.sender == truncateAddress(fulfillMsg.driver), 'invalid driver'); - - Order memory order = recreateOrder(cctpMsg, orderParams, extraParams); - bytes32 calculatedPayload = keccak256(encodeOrder(order)); - + bytes32 calculatedPayload = keccak256(encodeFulfillMsg(fulfillMsg).concat(abi.encodePacked(fulfillMsg.driver))); if (vm.payload.length != 32 || calculatedPayload != vm.payload.toBytes32(0)) { revert InvalidPayload(); } @@ -592,7 +617,7 @@ contract MayanCircle is ReentrancyGuard { amountOut ); - emit OrderFulfilled(order.cctpSourceDomain, order.cctpSourceNonce, amountOut); + emit OrderFulfilled(fulfillMsg.cctpSourceDomain, fulfillMsg.cctpSourceNonce, amountOut); } function refund( @@ -605,18 +630,21 @@ contract MayanCircle is ReentrancyGuard { (IWormhole.VM memory vm, bool valid, string memory reason) = wormhole.parseAndVerifyVM(encodedVm); require(valid, reason); - if (vm.emitterAddress != solanaEmitter - && vm.emitterAddress != suiEmitter - && truncateAddress(vm.emitterAddress) != address(this)) { - revert InvalidEmitter(); - } + validateEmitter(vm.emitterAddress); (address localToken, uint256 amount) = receiveCctp(cctpMsg, cctpSigs); - Order memory order = recreateOrder(cctpMsg, orderParams, extraParams); - bytes32 calculatedPayload = keccak256(encodeOrder(order)); - - if (vm.payload.length != 32 || calculatedPayload != vm.payload.toBytes32(0)) { + Order memory order = recreateOrder(orderParams, cctpMsg, extraParams); + bytes memory encodedOrder = encodeOrder(order); + OrderFields memory orderFields = OrderFields({ + referrerBps: orderParams.referrerBps, + protocolBps: extraParams.protocolBps, + cctpSourceNonce: cctpMsg.toUint64(CCTP_NONCE_INDEX), + cctpSourceDomain: cctpMsg.toUint32(CCTP_DOMAIN_INDEX) + }); + encodedOrder = encodedOrder.concat(encodeOrderFields(orderFields)); + encodedOrder = encodedOrder.concat(abi.encodePacked(cctpMsg.toUint64(CCTP_NONCE_INDEX), cctpMsg.toUint32(CCTP_DOMAIN_INDEX))); + if (vm.payload.length != 32 || keccak256(encodedOrder) != vm.payload.toBytes32(0)) { revert InvalidPayload(); } @@ -635,7 +663,7 @@ contract MayanCircle is ReentrancyGuard { IERC20(localToken).safeTransfer(msg.sender, order.redeemFee); IERC20(localToken).safeTransfer(destAddr, amount - order.redeemFee); - emit OrderRefunded(order.cctpSourceDomain, order.cctpSourceNonce, amount); + logRefund(cctpMsg, amount); } function receiveCctp(bytes memory cctpMsg, bytes memory cctpSigs) internal returns (address, uint256) { @@ -652,11 +680,24 @@ contract MayanCircle is ReentrancyGuard { return (localToken, amount); } + function getMintRecipient(uint32 destDomain, address tokenIn) internal view returns (bytes32 mintRecepient) { + mintRecepient = keyToMintRecipient[keccak256(abi.encodePacked(destDomain, tokenIn))]; + if (mintRecepient == bytes32(0)) { + revert MintRecepientNotSet(); + } + } + function getCaller(uint32 destDomain) internal view returns (bytes32 caller) { + caller = domainToCaller[destDomain]; + if (caller == bytes32(0)) { + revert CallerNotSet(); + } + } + function makePayments( FulfillMsg memory fulfillMsg, address tokenOut, uint256 amount - ) internal { + ) internal { address referrerAddr = truncateAddress(fulfillMsg.referrerAddr); uint256 referrerAmount = 0; if (referrerAddr != address(0) && fulfillMsg.referrerBps != 0) { @@ -695,33 +736,6 @@ contract MayanCircle is ReentrancyGuard { } } - function recreateOrder( - bytes memory cctpMsg, - OrderParams memory params, - ExtraParams memory extraParams - ) internal pure returns (Order memory) { - return Order({ - action: uint8(Action.SWAP), - payloadType: 1, - trader: extraParams.trader, - sourceChain: extraParams.sourceChainId, - tokenIn: cctpMsg.toBytes32(120), - amountIn: uint64(cctpMsg.toUint256(184)), - destAddr: params.destAddr, - destChain: params.destChain, - tokenOut: params.tokenOut, - minAmountOut: params.minAmountOut, - gasDrop: params.gasDrop, - redeemFee: params.redeemFee, - deadline: params.deadline, - referrerAddr: params.referrerAddr, - referrerBps: params.referrerBps, - protocolBps: extraParams.protocolBps, - cctpSourceNonce: cctpMsg.toUint64(12), - cctpSourceDomain: cctpMsg.toUint32(4) - }); - } - function encodeBridgeWithFee(BridgeWithFeeMsg memory bridgeMsg) internal pure returns (bytes memory) { return abi.encodePacked( bridgeMsg.action, @@ -737,25 +751,28 @@ contract MayanCircle is ReentrancyGuard { ); } - function parseBridgeWithFee(bytes memory payload) internal pure returns (BridgeWithFeeMsg memory) { + function recreateBridgeWithFee( + BridgeWithFeeParams memory bridgeParams, + bytes memory cctpMsg + ) internal pure returns (BridgeWithFeeMsg memory) { return BridgeWithFeeMsg({ action: uint8(Action.BRIDGE_WITH_FEE), - payloadType: payload.toUint8(1), - cctpNonce: payload.toUint64(2), - cctpDomain: payload.toUint32(10), - destAddr: payload.toBytes32(14), - gasDrop: payload.toUint64(46), - redeemFee: payload.toUint64(54), - burnAmount: payload.toUint64(62), - burnToken: payload.toBytes32(70), - customPayload: payload.toBytes32(102) + payloadType: bridgeParams.payloadType, + cctpNonce: cctpMsg.toUint64(CCTP_NONCE_INDEX), + cctpDomain: cctpMsg.toUint32(CCTP_DOMAIN_INDEX), + destAddr: bridgeParams.destAddr, + gasDrop: bridgeParams.gasDrop, + redeemFee: bridgeParams.redeemFee, + burnAmount: uint64(cctpMsg.toUint256(CCTP_AMOUNT_INDEX)), + burnToken: cctpMsg.toBytes32(CCTP_TOKEN_INDEX), + customPayload: bridgeParams.customPayload }); } function encodeUnlockFeeMsg(UnlockFeeMsg memory unlockMsg) internal pure returns (bytes memory) { return abi.encodePacked( unlockMsg.action, - unlockMsg.payloadId, + unlockMsg.payloadType, unlockMsg.cctpNonce, unlockMsg.cctpDomain, unlockMsg.unlockerAddr, @@ -766,7 +783,7 @@ contract MayanCircle is ReentrancyGuard { function encodeUnlockRefinedFeeMsg(UnlockRefinedFeeMsg memory unlockMsg) internal pure returns (bytes memory) { return abi.encodePacked( unlockMsg.action, - unlockMsg.payloadId, + unlockMsg.payloadType, unlockMsg.cctpNonce, unlockMsg.cctpDomain, unlockMsg.unlockerAddr, @@ -775,71 +792,10 @@ contract MayanCircle is ReentrancyGuard { ); } - function parseFulfillMsg(bytes memory encoded) public pure returns (FulfillMsg memory fulfillMsg) { - uint index = 0; - - fulfillMsg.action = encoded.toUint8(index); - index += 1; - - if (fulfillMsg.action != uint8(Action.FULFILL)) { - revert InvalidAction(); - } - - fulfillMsg.payloadId = encoded.toUint8(index); - index += 1; - - fulfillMsg.destChainId = encoded.toUint16(index); - index += 2; - - fulfillMsg.destAddr = encoded.toBytes32(index); - index += 32; - - fulfillMsg.driver = encoded.toBytes32(index); - index += 32; - - fulfillMsg.tokenOut = encoded.toBytes32(index); - index += 32; - - fulfillMsg.promisedAmount = encoded.toUint64(index); - index += 8; - - fulfillMsg.gasDrop = encoded.toUint64(index); - index += 8; - - fulfillMsg.referrerAddr = encoded.toBytes32(index); - index += 32; - - fulfillMsg.referrerBps = encoded.toUint8(index); - index += 1; - - fulfillMsg.protocolBps = encoded.toUint8(index); - index += 1; - - fulfillMsg.deadline = encoded.toUint64(index); - index += 8; - - fulfillMsg.redeemFee = encoded.toUint64(index); - index += 8; - - fulfillMsg.cctpDomain = encoded.toUint32(index); - index += 4; - - fulfillMsg.cctpNonce = encoded.toUint64(index); - index += 8; - } - - function parseOrderMsg(bytes memory payload) internal pure returns (OrderMsg memory) { - return OrderMsg({ - action: payload.toUint8(0), - payloadId: payload.toUint8(1), - orderHash: payload.toBytes32(2) - }); - } - function parseUnlockFeeMsg(bytes memory payload) internal pure returns (UnlockFeeMsg memory) { return UnlockFeeMsg({ action: payload.toUint8(0), - payloadId: payload.toUint8(1), + payloadType: payload.toUint8(1), cctpNonce: payload.toUint64(2), cctpDomain: payload.toUint32(10), unlockerAddr: payload.toBytes32(14), @@ -850,7 +806,7 @@ contract MayanCircle is ReentrancyGuard { function parseUnlockRefinedFee(bytes memory payload) internal pure returns (UnlockRefinedFeeMsg memory) { return UnlockRefinedFeeMsg({ action: payload.toUint8(0), - payloadId: payload.toUint8(1), + payloadType: payload.toUint8(1), cctpNonce: payload.toUint64(2), cctpDomain: payload.toUint32(10), unlockerAddr: payload.toBytes32(14), @@ -861,6 +817,8 @@ contract MayanCircle is ReentrancyGuard { function encodeOrder(Order memory order) internal pure returns (bytes memory) { return abi.encodePacked( + order.action, + order.payloadType, order.trader, order.sourceChain, order.tokenIn, @@ -872,20 +830,92 @@ contract MayanCircle is ReentrancyGuard { order.gasDrop, order.redeemFee, order.deadline, - order.referrerAddr, - order.referrerBps, - order.protocolBps + order.referrerAddr ); } - function encodeOrderMsg(OrderMsg memory orderMsg) internal pure returns (bytes memory) { + function encodeOrderFields(OrderFields memory orderFields) internal pure returns (bytes memory) { return abi.encodePacked( - orderMsg.action, - orderMsg.payloadId, - orderMsg.orderHash + orderFields.referrerBps, + orderFields.protocolBps, + orderFields.cctpSourceNonce, + orderFields.cctpSourceDomain ); } + function recreateOrder( + OrderParams memory params, + bytes memory cctpMsg, + ExtraParams memory extraParams + ) internal pure returns (Order memory) { + return Order({ + action: uint8(Action.SWAP), + payloadType: 1, + trader: extraParams.trader, + sourceChain: extraParams.sourceChainId, + tokenIn: cctpMsg.toBytes32(CCTP_TOKEN_INDEX), + amountIn: uint64(cctpMsg.toUint256(CCTP_AMOUNT_INDEX)), + destAddr: params.destAddr, + destChain: params.destChain, + tokenOut: params.tokenOut, + minAmountOut: params.minAmountOut, + gasDrop: params.gasDrop, + redeemFee: params.redeemFee, + deadline: params.deadline, + referrerAddr: params.referrerAddr + }); + } + + function encodeFulfillMsg(FulfillMsg memory fulfillMsg) internal pure returns (bytes memory) { + return abi.encodePacked( + fulfillMsg.action, + fulfillMsg.payloadType, + fulfillMsg.destAddr, + fulfillMsg.destChainId, + fulfillMsg.tokenOut, + fulfillMsg.promisedAmount, + fulfillMsg.gasDrop, + fulfillMsg.redeemFee, + fulfillMsg.deadline, + fulfillMsg.referrerAddr, + fulfillMsg.referrerBps, + fulfillMsg.protocolBps, + fulfillMsg.cctpSourceNonce, + fulfillMsg.cctpSourceDomain + ); + } + + function recreateFulfillMsg( + FulfillParams memory params, + bytes memory cctpMsg + ) internal pure returns (FulfillMsg memory) { + return FulfillMsg({ + action: uint8(Action.FULFILL), + payloadType: 1, + destAddr: params.destAddr, + destChainId: params.destChainId, + tokenOut: params.tokenOut, + promisedAmount: params.promisedAmount, + gasDrop: params.gasDrop, + redeemFee: params.redeemFee, + deadline: params.deadline, + referrerAddr: params.referrerAddr, + referrerBps: params.referrerBps, + protocolBps: params.protocolBps, + cctpSourceNonce: cctpMsg.toUint64(CCTP_NONCE_INDEX), + cctpSourceDomain: cctpMsg.toUint32(CCTP_DOMAIN_INDEX), + driver: params.driver + }); + } + + function validateEmitter(bytes32 emitter) internal { + if (emitter != solanaEmitter + && emitter != suiEmitter + && truncateAddress(emitter) != address(this)) { + revert InvalidEmitter(); + } + } + function maxApproveIfNeeded(address tokenAddr, address spender, uint256 amount) internal { IERC20 token = IERC20(tokenAddr); uint256 currentAllowance = token.allowance(address(this), spender); @@ -920,7 +950,11 @@ contract MayanCircle is ReentrancyGuard { amount *= 10 ** (decimals - 8); } return amount; - } + } + + function logRefund(bytes memory cctpMsg, uint256 amount) internal { + emit OrderRefunded(cctpMsg.toUint32(CCTP_DOMAIN_INDEX), cctpMsg.toUint64(CCTP_NONCE_INDEX), amount); + } function truncateAddress(bytes32 b) internal pure returns (address) { require(bytes12(b) == 0, 'invalid EVM address'); @@ -967,6 +1001,20 @@ contract MayanCircle is ReentrancyGuard { payEth(to, amount, true); } + function setDomainCaller(uint32 domain, bytes32 caller) public { + if (msg.sender != guardian) { + revert Unauthorized(); + } + domainToCaller[domain] = caller; + } + + function setMintRecipient(uint32 destDomain, address tokenIn, bytes32 mintRecipient) public { + if (msg.sender != guardian) { + revert Unauthorized(); + } + keyToMintRecipient[keccak256(abi.encodePacked(destDomain, tokenIn))] = mintRecipient; + } + function changeGuardian(address newGuardian) public { if (msg.sender != guardian) { revert Unauthorized(); From d55c6fe3ddde25550faacfefa7b92cf7ec23e162 Mon Sep 17 00:00:00 2001 From: real Date: Thu, 7 Nov 2024 13:14:59 +0300 Subject: [PATCH 3/6] fix refund recreate payload --- src/MayanCircle.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/src/MayanCircle.sol b/src/MayanCircle.sol index 351296a..e6f8a21 100644 --- a/src/MayanCircle.sol +++ b/src/MayanCircle.sol @@ -643,7 +643,6 @@ contract MayanCircle is ReentrancyGuard { cctpSourceDomain: cctpMsg.toUint32(CCTP_DOMAIN_INDEX) }); encodedOrder = encodedOrder.concat(encodeOrderFields(orderFields)); - encodedOrder = encodedOrder.concat(abi.encodePacked(cctpMsg.toUint64(CCTP_NONCE_INDEX), cctpMsg.toUint32(CCTP_DOMAIN_INDEX))); if (vm.payload.length != 32 || keccak256(encodedOrder) != vm.payload.toBytes32(0)) { revert InvalidPayload(); } From 92d12df752219e635afbe8ebd86def3615e4d585 Mon Sep 17 00:00:00 2001 From: real Date: Thu, 7 Nov 2024 21:11:40 +0300 Subject: [PATCH 4/6] accept full customPayload --- src/MayanCircle.sol | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/MayanCircle.sol b/src/MayanCircle.sol index e6f8a21..a92c928 100644 --- a/src/MayanCircle.sol +++ b/src/MayanCircle.sol @@ -240,7 +240,7 @@ contract MayanCircle is ReentrancyGuard { bytes32 destAddr, uint32 destDomain, uint8 payloadType, - bytes32 customPayload + bytes memory customPayload ) external payable nonReentrant returns (uint64 sequence) { if (paused) { revert Paused(); @@ -261,6 +261,11 @@ contract MayanCircle is ReentrancyGuard { getCaller(destDomain) ); + bytes32 customPayloadHash; + if (payloadType == 2) { + customPayloadHash = keccak256(customPayload); + } + BridgeWithFeeMsg memory bridgeMsg = BridgeWithFeeMsg({ action: uint8(Action.BRIDGE_WITH_FEE), payloadType: payloadType, @@ -271,7 +276,7 @@ contract MayanCircle is ReentrancyGuard { redeemFee: redeemFee, burnAmount: uint64(amountIn), burnToken: bytes32(uint256(uint160(tokenIn))), - customPayload: customPayload + customPayload: customPayloadHash }); bytes memory payload = abi.encodePacked(keccak256(encodeBridgeWithFee(bridgeMsg))); @@ -907,7 +912,7 @@ contract MayanCircle is ReentrancyGuard { }); } - function validateEmitter(bytes32 emitter) internal { + function validateEmitter(bytes32 emitter) view internal { if (emitter != solanaEmitter && emitter != suiEmitter && truncateAddress(emitter) != address(this)) { From 3c2c3fb1cfc8490c18e166ef9c95f77d07845010 Mon Sep 17 00:00:00 2001 From: real Date: Sun, 10 Nov 2024 13:42:06 +0300 Subject: [PATCH 5/6] fix bridgeWithLockedFee --- src/MayanCircle.sol | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/MayanCircle.sol b/src/MayanCircle.sol index a92c928..03bc88c 100644 --- a/src/MayanCircle.sol +++ b/src/MayanCircle.sol @@ -291,7 +291,8 @@ contract MayanCircle is ReentrancyGuard { uint256 amountIn, uint64 gasDrop, uint256 redeemFee, - uint32 destDomain + uint32 destDomain, + bytes32 destAddr ) external nonReentrant returns (uint64 cctpNonce) { if (paused) { revert Paused(); @@ -304,12 +305,10 @@ contract MayanCircle is ReentrancyGuard { IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn); maxApproveIfNeeded(tokenIn, address(cctpTokenMessenger), amountIn - redeemFee); - - bytes32 mintRecipient = getMintRecipient(destDomain, tokenIn); - cctpNonce = cctpTokenMessenger.depositForBurnWithCaller(amountIn - redeemFee, destDomain, mintRecipient, tokenIn, getCaller(destDomain)); + cctpNonce = cctpTokenMessenger.depositForBurnWithCaller(amountIn - redeemFee, destDomain, destAddr, tokenIn, getCaller(destDomain)); feeStorage[cctpNonce] = FeeLock({ - destAddr: mintRecipient, + destAddr: destAddr, gasDrop: gasDrop, token: tokenIn, redeemFee: redeemFee From 0eca21b378df3eed31100d76c1bcb436353e776d Mon Sep 17 00:00:00 2001 From: real Date: Sun, 8 Dec 2024 06:59:27 +0300 Subject: [PATCH 6/6] add set emitter function --- src/MayanCircle.sol | 112 +++++++++++++++++++++++++++++++------------- 1 file changed, 80 insertions(+), 32 deletions(-) diff --git a/src/MayanCircle.sol b/src/MayanCircle.sol index 03bc88c..a8e529f 100644 --- a/src/MayanCircle.sol +++ b/src/MayanCircle.sol @@ -23,8 +23,7 @@ contract MayanCircle is ReentrancyGuard { uint32 public immutable localDomain; uint16 public immutable auctionChainId; bytes32 public immutable auctionAddr; - bytes32 public immutable solanaEmitter; - bytes32 public immutable suiEmitter; + uint8 public consistencyLevel; address public guardian; address nextGuardian; @@ -32,18 +31,21 @@ contract MayanCircle is ReentrancyGuard { mapping(uint64 => FeeLock) public feeStorage; + mapping(uint16 => bytes32) public chainIdToEmitter; mapping(uint32 => bytes32) public domainToCaller; mapping(bytes32 => bytes32) public keyToMintRecipient; // key is domain + local token address uint8 constant ETH_DECIMALS = 18; + uint32 constant SOLANA_DOMAIN = 5; uint16 constant SOLANA_CHAIN_ID = 1; + uint32 constant SUI_DOMAIN = 8; - uint16 constant SUI_CHAIN_ID = 21; uint256 constant CCTP_DOMAIN_INDEX = 4; uint256 constant CCTP_NONCE_INDEX = 12; uint256 constant CCTP_TOKEN_INDEX = 120; + uint256 constant CCTP_RECIPIENT_INDEX = 152; uint256 constant CCTP_AMOUNT_INDEX = 184; event OrderFulfilled(uint32 sourceDomain, uint64 sourceNonce, uint256 amount); @@ -58,12 +60,17 @@ contract MayanCircle is ReentrancyGuard { error InvalidGasDrop(); error InvalidAction(); error InvalidEmitter(); + error EmitterAlreadySet(); error InvalidDestAddr(); error InvalidMintRecipient(); error InvalidRedeemFee(); error InvalidPayload(); error CallerNotSet(); - error MintRecepientNotSet(); + error MintRecipientNotSet(); + error MintRecipientAlreadySet(); + error InvalidCaller(); + error CallerAlreadySet(); + error DeadlineViolation(); enum Action { NONE, @@ -216,8 +223,6 @@ contract MayanCircle is ReentrancyGuard { address _feeManager, uint16 _auctionChainId, bytes32 _auctionAddr, - bytes32 _solanaEmitter, - bytes32 _suiEmitter, uint8 _consistencyLevel ) { cctpTokenMessenger = ITokenMessenger(_cctpTokenMessenger); @@ -225,8 +230,6 @@ contract MayanCircle is ReentrancyGuard { feeManager = IFeeManager(_feeManager); auctionChainId = _auctionChainId; auctionAddr = _auctionAddr; - solanaEmitter = _solanaEmitter; - suiEmitter = _suiEmitter; consistencyLevel = _consistencyLevel; localDomain = ITokenMessenger(_cctpTokenMessenger).localMessageTransmitter().localDomain(); guardian = msg.sender; @@ -373,13 +376,18 @@ contract MayanCircle is ReentrancyGuard { }(0, payload, consistencyLevel); } - function redeemWithFee(bytes memory cctpMsg, bytes memory cctpSigs, bytes memory encodedVm, BridgeWithFeeParams memory bridgeParams) external nonReentrant payable { + function redeemWithFee( + bytes memory cctpMsg, + bytes memory cctpSigs, + bytes memory encodedVm, + BridgeWithFeeParams memory bridgeParams + ) external nonReentrant payable { (IWormhole.VM memory vm, bool valid, string memory reason) = wormhole.parseAndVerifyVM(encodedVm); require(valid, reason); - validateEmitter(vm.emitterAddress); + validateEmitter(vm.emitterAddress, vm.emitterChainId); - if (truncateAddress(cctpMsg.toBytes32(152)) != address(this)) { + if (truncateAddress(cctpMsg.toBytes32(CCTP_RECIPIENT_INDEX)) != address(this)) { revert InvalidMintRecipient(); } @@ -418,9 +426,9 @@ contract MayanCircle is ReentrancyGuard { } function redeemWithLockedFee(bytes memory cctpMsg, bytes memory cctpSigs, bytes32 unlockerAddr) external nonReentrant payable returns (uint64 sequence) { - uint32 cctpSourceDomain = cctpMsg.toUint32(4); - uint64 cctpNonce = cctpMsg.toUint64(12); - address mintRecipient = truncateAddress(cctpMsg.toBytes32(152)); + uint32 cctpSourceDomain = cctpMsg.toUint32(CCTP_DOMAIN_INDEX); + uint64 cctpNonce = cctpMsg.toUint64(CCTP_NONCE_INDEX); + address mintRecipient = truncateAddress(cctpMsg.toBytes32(CCTP_RECIPIENT_INDEX)); if (mintRecipient == address(this)) { revert InvalidMintRecipient(); } @@ -483,7 +491,7 @@ contract MayanCircle is ReentrancyGuard { (IWormhole.VM memory vm, bool valid, string memory reason) = wormhole.parseAndVerifyVM(encodedVm); require(valid, reason); - validateEmitter(vm.emitterAddress); + validateEmitter(vm.emitterAddress, vm.emitterChainId); unlockMsg.action = uint8(Action.UNLOCK_FEE); bytes32 calculatedPayload = keccak256(encodeUnlockFeeMsg(unlockMsg)); @@ -514,7 +522,7 @@ contract MayanCircle is ReentrancyGuard { (IWormhole.VM memory vm1, bool valid1, string memory reason1) = wormhole.parseAndVerifyVM(encodedVm1); require(valid1, reason1); - validateEmitter(vm1.emitterAddress); + validateEmitter(vm1.emitterAddress, vm1.emitterChainId); unlockMsg.action = uint8(Action.UNLOCK_FEE); bytes32 calculatedPayload1 = keccak256(encodeUnlockFeeMsg(unlockMsg)); @@ -532,7 +540,7 @@ contract MayanCircle is ReentrancyGuard { (IWormhole.VM memory vm2, bool valid2, string memory reason2) = wormhole.parseAndVerifyVM(encodedVm2); require(valid2, reason2); - validateEmitter(vm2.emitterAddress); + validateEmitter(vm2.emitterAddress, vm2.emitterChainId); refinedMsg.action = uint8(Action.UNLOCK_FEE_REFINE); bytes32 calculatedPayload2 = keccak256(encodeUnlockRefinedFeeMsg(refinedMsg)); @@ -569,11 +577,17 @@ contract MayanCircle is ReentrancyGuard { require(valid, reason); require(vm.emitterChainId == SOLANA_CHAIN_ID, 'invalid emitter chain'); - require(vm.emitterAddress == auctionAddr, 'invalid solana emitter'); + if (vm.emitterAddress != auctionAddr) { + revert InvalidEmitter(); + } FulfillMsg memory fulfillMsg = recreateFulfillMsg(params, cctpMsg); - require(fulfillMsg.deadline >= block.timestamp, 'deadline passed'); - require(msg.sender == truncateAddress(fulfillMsg.driver), 'invalid driver'); + if (fulfillMsg.deadline < block.timestamp) { + revert DeadlineViolation(); + } + if (msg.sender != truncateAddress(fulfillMsg.driver)) { + revert Unauthorized(); + } bytes32 calculatedPayload = keccak256(encodeFulfillMsg(fulfillMsg).concat(abi.encodePacked(fulfillMsg.driver))); if (vm.payload.length != 32 || calculatedPayload != vm.payload.toBytes32(0)) { @@ -634,7 +648,7 @@ contract MayanCircle is ReentrancyGuard { (IWormhole.VM memory vm, bool valid, string memory reason) = wormhole.parseAndVerifyVM(encodedVm); require(valid, reason); - validateEmitter(vm.emitterAddress); + validateEmitter(vm.emitterAddress, vm.emitterChainId); (address localToken, uint256 amount) = receiveCctp(cctpMsg, cctpSigs); @@ -651,7 +665,9 @@ contract MayanCircle is ReentrancyGuard { revert InvalidPayload(); } - require(order.deadline < block.timestamp, 'deadline not passed'); + if (order.deadline >= block.timestamp) { + revert DeadlineViolation(); + } uint256 gasDrop = deNormalizeAmount(order.gasDrop, ETH_DECIMALS); if (msg.value != gasDrop) { @@ -683,16 +699,24 @@ contract MayanCircle is ReentrancyGuard { return (localToken, amount); } - function getMintRecipient(uint32 destDomain, address tokenIn) internal view returns (bytes32 mintRecepient) { - mintRecepient = keyToMintRecipient[keccak256(abi.encodePacked(destDomain, tokenIn))]; - if (mintRecepient == bytes32(0)) { - revert MintRecepientNotSet(); + function getMintRecipient(uint32 destDomain, address tokenIn) internal view returns (bytes32) { + bytes32 mintRecepient = keyToMintRecipient[keccak256(abi.encodePacked(destDomain, tokenIn))]; + if (mintRecepient != bytes32(0)) { + return mintRecepient; + } else if (destDomain == SOLANA_DOMAIN || destDomain == SUI_DOMAIN) { + revert MintRecipientNotSet(); + } else { + return bytes32(uint256(uint160(address(this)))); } } function getCaller(uint32 destDomain) internal view returns (bytes32 caller) { caller = domainToCaller[destDomain]; - if (caller == bytes32(0)) { + if (caller != bytes32(0)) { + return caller; + } else if (destDomain == SOLANA_DOMAIN || destDomain == SUI_DOMAIN) { revert CallerNotSet(); + } else { + return bytes32(uint256(uint160(address(this)))); } } @@ -911,10 +935,8 @@ contract MayanCircle is ReentrancyGuard { }); } - function validateEmitter(bytes32 emitter) view internal { - if (emitter != solanaEmitter - && emitter != suiEmitter - && truncateAddress(emitter) != address(this)) { + function validateEmitter(bytes32 emitter, uint16 chainId) view internal { + if (emitter != chainIdToEmitter[chainId] && truncateAddress(emitter) != address(this)) { revert InvalidEmitter(); } } @@ -1008,6 +1030,12 @@ contract MayanCircle is ReentrancyGuard { if (msg.sender != guardian) { revert Unauthorized(); } + if (caller == bytes32(0)) { + revert InvalidCaller(); + } + if (domainToCaller[domain] != bytes32(0)) { + revert CallerAlreadySet(); + } domainToCaller[domain] = caller; } @@ -1015,7 +1043,27 @@ contract MayanCircle is ReentrancyGuard { if (msg.sender != guardian) { revert Unauthorized(); } - keyToMintRecipient[keccak256(abi.encodePacked(destDomain, tokenIn))] = mintRecipient; + if (mintRecipient == bytes32(0)) { + revert InvalidMintRecipient(); + } + bytes32 key = keccak256(abi.encodePacked(destDomain, tokenIn)); + if (keyToMintRecipient[key] != bytes32(0)) { + revert MintRecipientAlreadySet(); + } + keyToMintRecipient[key] = mintRecipient; + } + + function setEmitter(uint16 chainId, bytes32 emitter) public { + if (msg.sender != guardian) { + revert Unauthorized(); + } + if (emitter == bytes32(0)) { + revert InvalidEmitter(); + } + if (chainIdToEmitter[chainId] != bytes32(0)) { + revert EmitterAlreadySet(); + } + chainIdToEmitter[chainId] = emitter; } function changeGuardian(address newGuardian) public {